經常使用算法解析技巧總結

算法是程序的靈魂。一般的程序主要是由算法與數據結構組成。算法解法變幻無窮,學習曲線陡,解題沒有固定的模式,這些也是算法的魅力所在。在此總結一下算法的經常使用技巧。算法

1. 巧用數組下標數組

   數組的下標是一個隱含的頗有用的數組,特別是在統計一些數字或者判斷一些整型數是否出現過的時候。例如,給你一串字母,讓你判斷這些字母出現的次數時,就能夠把這些字母映射做爲下標,在遍歷的時候,若是字母a遍歷到,則arr[a]就能夠加1了,即  arr[a]++,獲得的數組值就是字母出現的次數;數據結構

  經過這種巧用下標的方法,咱們不須要遍歷每一個字母去判斷,時間複雜度是O(n),以空間複雜度換取了時間複雜度。學習

例子: 優化

  問題:給你n個無序的int整型數組arr,而且這些整數的取值範圍都在0-20之間,要你在 O(n) 的時間複雜度中把這 n 個數按照從小到大的順序打印出來。spa

  這道題,若是你是先把這 n 個數先排序,再打印,是不可能O(n)的時間打印出來的。可是數值範圍在 0-20。就能夠巧用數組下標。把對應的數值做爲數組下標,若是這個數出現過,則對應的數組加1。指針

 代碼以下:code

public static void orderPrint(int arr[])
    {
        int[] temp = new int[21];
        //統計出現次數
        for (int i = 0; i < arr.length; i++)
        {
            temp[arr[i]]++;
        }
        //順序打印
        for (int i = 0; i < 21; i++)
        {
            for (int j = 0; j < temp[i]; j++)
            {
                System.out.println(i);
            }
        }
    }

數組下標通常用於統計場景,其餘狀況也能夠考慮是否能夠巧用數組下標來優化。blog

2. 巧用取餘排序

 有時候咱們在遍歷數組的時候,會進行越界判斷,若是下標差很少要越界了,咱們就把它置爲0從新遍歷。特別是在一些環形的數組中,例如用數組實現的隊列。每每會寫出這樣的代碼:

for (int i = 0; i < N; i++)
        {
            if (pos < N)
            {
                // 沒有越界,使用數組arr[pos]
            }
            else
            {
                pos = 0;//置爲0再使用數組
                //使用arr[pos]
            }
            pos++;
        }

實際上咱們能夠經過取餘的方法來簡化代碼

        for (int i = 0; i < N; i++)
        {
            //使用數組arr[pos]   (剛開始的時候是pos < N)
            pos = (pos + 1) % N;
        }

3. 巧用雙指針

   對於雙指針,在作關於單鏈表的題是特別有用,好比「判斷單鏈表是否有環」、「如何一次遍歷就找到鏈表中間位置節點」、「單鏈表中倒數第 k 個節點」等問題。對於這種問題,咱們就可使用雙指針了,會方便不少。我順便說下這三個問題怎麼用雙指針解決吧。

例如對於第一個問題

   咱們就能夠設置一個慢指針和一個快指針來遍歷這個鏈表。慢指針一次移動一個節點,而快指針一次移動兩個節點,若是該鏈表沒有環,則快指針會先遍歷完這個表,若是有環,則快指針會在第二次遍歷時和慢指針相遇。

對於第二個問題 

  同樣是設置一個快指針和慢指針。慢的一次移動一個節點,而快的兩個。在遍歷鏈表的時候,當快指針遍歷完成時,慢指針恰好達到中點。

對於第三個問題

   設置兩個指針,其中一個指針先移動k個節點。以後兩個指針以相同速度移動。當那個先移動的指針遍歷完成的時候,第二個指針正好處於倒數第k個節點。

你看,採用雙指針方便多了吧。因此之後在處理與鏈表相關的一些問題的時候,能夠考慮雙指針哦。

4. 巧用哈希表 哈希表最好的狀況下空間複雜度能夠下降到 O(1),最壞的狀況仍然了 O(N)

5. 巧用移位運算

   有時候咱們在進行除數或乘數運算的時候,例如n / 2,n / 4, n / 8這些運算的時候,咱們就能夠用移位的方法來運算了,這樣會快不少。

  例如:

    n / 2 等價於 n >> 1

    n / 4 等價於 n >> 2

    n / 8 等價於 n >> 3。 

    還有一些 &(與)、|(或)的運算,也能夠加快運算的速度。例如判斷一個數是不是奇數,你可能會這樣作

if (n % 2 == 1)
        {

            //TODO
        }

  不過咱們用與或運算的話會快不少。例如判斷是不是奇數,咱們就能夠把n和1相與了,若是結果爲1,則是奇數,不然就不會。即

if ((n & 1) == 1)
        {
            //TODO
        }

  具體的一些運算技巧,還得須要多在實踐中嘗試着去使用,這樣用久後就會比較熟練。

6. 設置哨兵位

   在鏈表的相關問題中,咱們常常會設置一個頭指針,並且這個頭指針是不存任何有效數據的,只是爲了操做方便,這個頭指針咱們就能夠稱之爲哨兵位了。

   例如咱們要刪除頭第一個節點是時候,若是沒有設置一個哨兵位,那麼在操做上,它會與刪除第二個節點的操做有所不一樣。可是咱們設置了哨兵,那麼刪除第一個節點和刪除第二個節點那麼在操做上就同樣了,不用作額外的判斷。固然,插入節點的時候也同樣。

   有時候咱們在操做數組的時候,也是能夠設置一個哨兵的,把arr[0]做爲哨兵。例如,要判斷兩個相鄰的元素是否相等時,設置了哨兵就不怕越界等問題了,能夠直接arr[i] == arr[i-1]?了。不用怕i = 0時出現越界。

   具體的應用還有不少,例如插入排序,環形鏈表等。

6. 與遞歸有關的一些優化

(1).對於能夠遞歸的問題考慮狀態保存

 當咱們使用遞歸來解決一個問題的時候,容易產生重複去算同一個子問題,這個時候咱們要考慮狀態保存以防止重複計算。例如

 問題:一隻青蛙一次能夠跳上1級臺階,也能夠跳上2級。求該青蛙跳上一個n級的臺階總共有多少種跳法?

 這個問題用遞歸很好解決。假設 f(n) 表示n級臺階的總跳數法,則有

  f(n) = f(n-1) + f(n - 2)

 遞歸的結束條件是當0 <= n <= 2時, f(n) = n。所以咱們能夠很容易寫出遞歸的代碼

public int leap(int n)
    {
        if (n <= 2)
        {
            return n;
        }
        else
        {
            return leap(n - 1) + leap(n - 2);
        }
    }

   不過對於可使用遞歸解決的問題,咱們必定要考慮是否有不少重複計算。顯然對於 f(n) = f(n-1) + f(n-2) 的遞歸,是有不少重複計算的。這個時候咱們要考慮狀態保存。例如用hashMap來進行保存,固然用一個數組也是能夠的,這個時候就像咱們上面說的巧用數組下標了。能夠當arr[n] = 0時,表示n還沒計算過,當arr[n] != 0時,表示f(n)已經計算過,這時就能夠把計算過的值直接返回回去了。所以咱們考慮用狀態保存的作法代碼以下:

//數組的大小根據具體狀況來,因爲int數組元素的的默認值是0,所以不用初始化
    int[] arr = new int[1000];

    public int leap(int n)
    {
        if (n <= 2)
        {
            return n;
        }
        else
        {
            if (arr[n] != 0)
            {
                return arr[n];//已經計算過,直接返回
            }
            else
            {
                arr[n] = leap(n - 1) + leap(n - 2);
                return arr[n];
            }
        }
    }

  這樣,能夠極大着提升算法的效率。也有人把這種狀態保存稱之爲備忘錄法

 (2).考慮自底向上

   對於遞歸的問題,咱們通常都是從上往下遞歸的,直到遞歸到最底,再一層一層着把值返回。

  不過,有時候當n比較大的時候,例如當 n = 10000時,那麼必需要往下遞歸10000層直到 n <=2 纔將結果慢慢返回,若是n太大的話,可能棧空間會不夠用。

   對於這種狀況,其實咱們是能夠考慮自底向上的作法的。例如我知道

   f(1) = 1;

  f(2) = 2;

   那麼咱們就能夠推出 f(3) = f(2) + f(1) = 3。從而能夠推出f(4),f(5)等直到f(n)。所以,咱們能夠考慮使用自底向上的方法來作。

   代碼以下:

public int leap(int n)
    {
        if (n <= 2)
            return n;
        int f1 = 1;
        int f2 = 2;
        int sum = 0;
        for (int i = 3; i <= n; i++)
        {
            sum = f1 + f2;
            f1 = f2;
            f2 = sum;
        }
        return sum;
    }

   咱們也把這種自底向上的作法稱之爲遞推。

  當在使用遞歸解決問題的時候,要考慮如下兩個問題:

     (1). 是否有狀態重複計算的,可不可使用備忘錄法來優化。

     (2). 是否能夠採起遞推的方法來自底向上作,減小一味遞歸的開銷。

相關文章
相關標籤/搜索