劍指Offer-34-把陣列排成最小的數

SpecialYang發表於2018-07-30

題目

輸入一個正整數陣列,把陣列裡所有數字拼接起來排成一個數,列印能拼接出的所有數字中最小的一個。例如輸入陣列{3,32,321},則列印出這三個數字能排成的最小數字為321323。

解析

此題的目的是如何找到這些數的所有的排列中最小的字典序的那一個排列。所以問題轉化為如何找到字典序最小的排列。為了防止組成的數溢位,下面統一轉換為字串來做。

思路一

既然需要找到字典序最小的哪個排列,那麼我們乾脆一不做二不休,我求出所有的排列組合,然後排序後取最小不就得了。進一步轉化問題為全排列問題,只不過參與排列的元素為字串了,而不在是單純的單個字元。
求全排列的演算法我已經在這篇部落格寫的很清楚了。以下內容為那篇部落格裡最優的求全排列的解法,包含如何對於重複元素的處理防止不必要的處理操作。
可以把排列問題分成固定第一個位置,剩餘元素全排列問題。之後對剩餘元素又進行同樣的處理,固定第一個位置,剩餘元素全排列。如圖:

這裡寫圖片描述
1,2,3的全排列問題,可以看做1與23的全排列,2與13的全排列,3與21的全排列的總和。也就是我們可以把原始排列問題劃分為2部分,第一部分固定,第二部分為剩餘元素全排列。所以只要讓第一部分不斷與後面的交換元素,然後繼續處理當前新組成第二部分的全排列問題即可。而我們求剩餘元素的全排列的時候,又可以按下面劃分為2部分繼續搞,直到剩餘的元素個數為1,那麼當前組成一個排列。這時向上回溯即可,開始更新各個子問題的第一部分的元素,繼續處理新的排列問題。
能不能優化一下,不產生多餘的排列呢。
第一個想法可能就是遇到與第一部分元素相等,就不交換。比如abb, 第一個位置可分別於第二個,第三個位置交換,因為他們都不與a相同,得到 abb, bab, bba。 考察第二位置時,對於bab,第二位置會與第三個位置交換,得到bba。而bba已在與第一位置交換的過程中出現過了。所以單純的看交換元素是否相等是不行的。
我們發現導致有重複的出現的原因為,已經有b在第一個位置出現過了,不能在有相同的元素交換到此位置上,即不能有重複的元素作為排列問題的第一部分,這肯定會導致重複的子問題產生。所以對於每一個位置遍歷,我們新增一個set用於記錄以在該位置出現過的元素。 對於得到的全排列進行排序,取第一個就是我們所要的字典序最小的排列。

    public static String PrintMinNumber(int [] numbers) {
        if(numbers == null || numbers.length == 0) {
            return "";
        }
        List<String> result = new ArrayList<>();
        getAllCombine(0, numbers, result);
        Collections.sort(result);
        return result.get(0);
    }

    private static void getAllCombine(int index, int[] numbers, List<String> result) {
        if(index == numbers.length - 1) {
            StringBuilder sb = new StringBuilder();
            for(int num : numbers) {
                sb.append(num);
            }
            result.add(sb.toString());
            return;
        }
        //記錄已經出現第一部分的元素
        Set<Integer> ocur = new HashSet<>();
        for(int i = index; i < numbers.length; i++) {
            if(!(i != index && ocur.contains(numbers[i]))) {
                ocur.add(numbers[i]);
                swap(numbers, i, index);
                getAllCombine(index + 1, numbers, result);
                swap(numbers, i, index);
            }
        }
    }

    public static void swap(int[] temp, int i, int j) {
        if(i != j) {
            int str = temp[i];
            temp[i] = temp[j];
            temp[j] = str;
        }
    }
複製程式碼

思路二

思路一的複雜度為O(n!),那麼我們能不能不進行全排列就能找到那個特定的排列呢?很容易想到就是排序,如果能想到一個很好的排序策略,即可優雅得到最後的結果。
對於給定的2個數,a和b。如何確定兩者之間的排序策略呢?我們可以發現這兩者的排列為:ab,ba。我們最終目的是找到字典序最小的那個排列,所以我們肯定應該保持這種關係,從而決定是否交換順序:

  1. 當ab < ba, a排在b的左邊
  2. 當ab > ba, b排在a的左邊
  3. 當ab = ba, 位置關係隨意

不理解的話,可以結合選擇排序演算法來理解,我們在n個元素選擇出最小的作為左邊的值,顯然它應該位於所有n-1個元素的左邊,也就是第一個位置上。接著在n-1個元素找到次最小,反覆如此,直到沒有剩餘元素。
如果你理解了選擇排序的做法,又因為一般的排序演算法的結果都是一樣,所以我們可以採用更快的排序演算法來解答此題,為了方便,直接了使用了庫函式,當然你可以自行實現快速排序或歸併排序,這些都未嘗不可。

    public static String PrintMinNumber2(int [] numbers) {
        if(numbers == null || numbers.length == 0) {
            return "";
        }
        String[] strNumbers = new String[numbers.length];
        for(int i = 0; i < numbers.length; i++) {
            strNumbers[i] = String.valueOf(numbers[i]);
        }
        Arrays.sort(strNumbers, new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                return (o1 + o2).compareTo(o2 + o1);
            }
        });
        StringBuilder sb = new StringBuilder();
        for(int i = 0; i < strNumbers.length; i++) {
            sb.append(strNumbers[i]);
        }
        return sb.toString();
    }
複製程式碼

總結

對於給定一組數,讓求特定的排列時,這時肯定可以用全排列來做,只不過涉及到效率問題。進一步優化則可以往深處深究。

相關文章