解題技巧-發現子結構

算法問題有很大一部分均可以將問題歸約成子問題,經過解決子問題而解決原問題。這一策略常見的使用場景是divide and conquer,這一策略也能夠輕易地用來解決不少其它問題。本文經過Rotate Array這一例子講解這個技巧。面試

Rotate Array

https://leetcode.com/problems/rotate-array/算法

Rotate an array of n elements to the right by k steps.數組

For example, with n = 7 and k = 3, the array [1,2,3,4,5,6,7] is rotated to [5,6,7,1,2,3,4].ide


分析

問題的大意就是,將數組的後面一部分挪到前面。咱們能夠簡單地分配一個新數組,將後一部分拷貝到新數組頭部,再將前面一部分拷貝到後面。這種作法簡單,而且也很是容易並行。然而,須要額外的空間開銷。面試官極可能會要求你給出in place的方法。怎麼辦呢?優化

先考慮簡單的狀況,很容易想到的一種情形,k是n的倍數,那麼根本不用挪。除此以外,還有一種簡單情形,n是偶數,k=n/2。這種情形只須要交換i和n-k+i位置的數就能夠。編碼

第一種簡單情形看起來沒啥用處。第二種情形又能給咱們解決通常情形帶來什麼啓發呢?對於array=[1,2,3,4,5,6],k=3的狀況,咱們只須要交換先後兩半的元素,就能夠獲得[4,5,6,1,2,3]。那麼對於array=[1,2,3,4,5,6,7],k=3的狀況呢?翻譯

沒什麼思路,不過無妨先按照前面的辦法,先將1,2,3和5,6,7交換看看,這樣就變成了[5,6,7,4,1,2,3]。這個時候咱們能夠發現,5,6,7已經到了最終位置(咱們最終要的是[5,6,7,1,2,3,4]),如今咱們還要作的就是將[4,1,2,3]變成[1,2,3,4]。也許你還在思考接下來該怎麼辦,可是我要告訴你,問題其實已經解決了。爲何你沒看出來呢?缺乏善於發現子結構的眼睛啊騷年!其實[4,1,2,3]->[1,2,3,4]不就是原來的問題中n=4,k=3的情形嗎?code


編碼

若是用遞歸直接翻譯上面思路的編碼,那麼遞歸調用棧仍然可能用上O(n)的空間。遞歸

void rotate(int nums[], int n, int k) {
    k=k%n;
    if (n==0 || k == 0) return;
    
    if (k<=n-k) { //first part is longer
        for (int a = 0; a < k; ++a) {
            swap(nums[a], nums[a+n-k]);
        }
        rotate(nums+k, n-k, k);
    } else { //second part is longer
        for (int a = 0; a < n-k; ++a) {
            swap(nums[a], nums[a+n-k]);
        }
        rotate(nums+n-k, k, k-(n-k));
    }
}

固然編譯器可能會發現這是個尾遞歸,從而使得實際的空間使用爲常數。不過面試官接不接受就是另外一回事兒了,興許他會問你編譯器是如何作這個優化的。element

咱們也能夠主動提出手動作這個優化,而且能夠作得更漂亮。觀察代碼,咱們能夠發現,遞歸調用的三個參數在變。而這三個參數無非指明瞭先後兩段,咱們要作的是把後一段挪到前一段。咱們須要記錄的是前一段從哪開始,中點在哪,以及後一段從哪開始。而且當前面一段用來交換的遊標碰到中點或者後面一段用來交換的遊標碰到結尾的時候注意更新。

void rotate(int nums[], int n, int k) {
    k = k%n;
    if (k==0) return;
    int first = 0;
    int second = n-k;
    int middle = n-k;
    while (first != second) {
        swap(nums[first], nums[second]);
        first++;second++;
        if (second == n) {
            second = middle;
        } else if (first == middle) {
            middle = second;
        }
    }
}


使用要義

有意識地注意子結構。若是咱們不去有意識地注意子結構,可能在參考第二種簡單情形同樣進行部分交換以後仍然不知所措。

相似斐波那契數列這樣的問題,因爲子結構由一個變量(數列長度)便可定義,咱們可能很快就能發現。有時候子結構可能會像這個問題同樣,須要三個變量才能定義,也要能及時發現纔好。


相關內容

對於這個問題,還有一種時間O(nk),空間O(1)的辦法,將最後一個挪到第一個,也就是拿出來,將全部元素後移一位,放到第一個,重複作k遍。

還有一個時間O(n),空間O(1)的作法,將前面半段翻轉,後面半段翻轉,整個翻轉。這是一個經典作法,不過反正我想不到。

子結構的應用很是普遍,凡涉及到遞歸的幾乎都有其用武之地,有兩個常見的地方:divide and conquer和dynamic programming。

對於這個問題,咱們先解決問題的一部分,而後解決子問題。有時候,也相反,先解決子問題,再用子問題的結果得出原問題的結果,好比dynamic programming。這是兩個方向。

相關文章
相關標籤/搜索