編程算法思想小結

所謂編程算法思想,就是看到一道編程題後你有什麼與此題有關的算法思想。
以下我分4種思想並結合具體例題來分別進行闡述。
1.0,1思想:在某些編程問題需要表示事物兩種不同的狀態,這時候就需要用到0,1思想。
例1:有n個人圍成一圈,順序排號。從第一個人開始報數(從1到3報數),凡報到3的人退出圈子,問最後留下的是原來第幾號的那位?
這個題中顯然有在場和離場兩種狀態,此時就可以用1表示在場(數組初始化爲1),
用0表示離場表示被淘汰離場(按條件每計數到3的數組就賦值爲0)。有了這個思想作爲此題的突破口,具體代碼也就迎刃而解了。
下面給出此題與此相關的部分核心代碼:

for (int i = 0; i < n; i++) {
            arr[i] = 1;
        }
        int num = n;
        int count = 0;
        while (num > 1) {
            for (int i = 0; i < n; i++) {
                if (arr[i] == 1) {
                    count++;
                }
                if (count == 3) {
                    arr[i] = 0;
                    num--;
                    count = 0;
                }
            }
        }

例2:(不同路徑問題)一個機器人位於一個 m x n 網格的左上角 。機器人每次只能向下或者向右移動一步。現在考慮網格中有障礙物。那麼從左上角到右下角將會有多少條不同的路徑?網格中的障礙物和空位置分別用 1 和 0 來表示。
此題題目就已經用0,1分別表示空位置和障礙物的兩種狀態了,然後再結合簡單動態規劃思想,此題也便不難求解。
下面給出此題與此相關的部分核心代碼:

for(int i = 0; i<obstacleGrid.length;i++){
            for(int j = 0 ; j<obstacleGrid[i].length;j++){
                if(obstacleGrid[i][j] == 1){//操作障礙點(包括邊緣的障礙點)
                    obstacleGrid[i][j] = 0;
                    continue;//很重要,使得一個點只會被判斷進行一次操作
                }
                if(i == 0 && j == 0){//操作起點
                    obstacleGrid[i][j] = 1;
                    continue;
                }
                if(i == 0 || j == 0){//操作邊緣點(如果遇到有障礙點,則後面的點都爲0)
                    obstacleGrid[i][j] = i == 0?obstacleGrid[i][j-1]:obstacleGrid[i-1][j];
                    continue;
                }
                obstacleGrid[i][j] = obstacleGrid[i-1][j]+obstacleGrid[i][j-1];//操作正常點
            }
        }

此外,0,1思想還有一個重要的用途,在某些遍歷上。例如圖,爲了避免重複遍歷,先把所有的節點初始化爲0,每遍歷一個就賦值爲1。

2.雙/三指針思想:在某些編程問題需要用多個變量來同時表示一個事物,這時候就需要用到雙/三指針思想。
例1:給定一個數組 nums 和一個值 val,你需要原地移除所有數值等於 val 的元素,返回移除後數組的新長度。不要使用額外的數組空間,你必須在原地修改輸入數組並在使用 O(1) 額外空間的條件下完成。
由於只能在原數組上進行修改,但是又要遍歷原數組,所有此時一個指針變量以及滿足不了需求了,便使用雙指針來解決此問題。
下面給出此題與此相關的部分核心代碼:

for (int j = 0; j < nums.length; j++) {
            if (nums[j] != val) {
                nums[i] = nums[j];
                i++;
            }
        }

例2:編寫一個程序,找出第 n 個醜數。醜數就是隻包含質因數 2, 3, 5 的正整數。
此題需要再創建一個數組來存儲醜數,但是由於醜數有三個約束條件,所以此時需要創建三個指針來分別指向2的倍數,3的倍數和5的倍數,有了這三個指針後面的編程也就變得簡單了許多。
下面給出此題與此相關的部分核心代碼:

for (int i = 1;i < n;i++) {
            int multiply2 = result[divisor2] * 2;
            int multiply3 = result[divisor3] * 3;
            int multiply5 = result[divisor5] * 5;
            int min = Math.min(multiply2,Math.min(multiply3,multiply5));
            result[i] = min;
            if (multiply2 == min) {
                divisor2++;
            }
            if (multiply3 == min) {
                divisor3++;
            }
            if (multiply5 == min) {
                divisor5++;
            }
        }

例3:給定一個包含 n 個整數的數組 nums,判斷 nums 中是否存在三個元素 a,b,c ,使得 a + b + c = 0 ?找出所有滿足條件且不重複的三元組。注意:答案中不可以包含重複的三元組。
這題也需要創建三個指針,一個用於表示遍歷數組的指針,一個用於順方向的比較的指針,另一個用於逆方向比較的指針,有了這三個指針後面的編程也就變得簡單了許多。
下面給出此題與此相關的部分核心代碼:

 for (int i = 0; i < nums.length - 2; i++) {
            if (i == 0 || (nums[i] != nums[i - 1])) {  // 跳過重複的nums[i]
                int l = i + 1, r = nums.length - 1, sum = 0 - nums[i];
                while (l < r) {
                    if (nums[l] + nums[r] == sum) {
                        ls.add(Arrays.asList(nums[i], nums[l], nums[r]));
                        while (l < r && nums[l] == nums[l + 1]) l++;// 跳過重複的nums[l]
                        while (l < r && nums[r] == nums[r - 1]) r--;// 跳過重複的nums[r]
                        l++;
                        r--;
                    } else if (nums[l] + nums[r] < sum) {
                        while (l < r && nums[l] == nums[l + 1]) l++;// 跳過重複的nums[l]
                        l++;
                    } else {
                        while (l < r && nums[r] == nums[r - 1]) r--;// 跳過重複的nums[r]
                        r--;
                    }
                }
            }
        }

此外,在鏈表等許多地方也用到了多指針的思想,例如反轉單鏈表的兩種方法就1都需要用到三指針的思想。

3.棧(遞歸)/隊列思想:顧名思義,此類思想需要用到棧(先進後出),隊列(先進先出)的思想來解決問題。
例1:給定一個只包括 ‘(’,’)’,’{’,’}’,’[’,’]’ 的字符串,判斷字符串是否有效。
由於括號是就近的匹配,所以此題需要用到棧來解決。可以先將字符串的左括號全都放入一個棧中,然後再依次取出和右括號進行匹配,此題也便迎刃而解了。
下面給出此題與此相關的部分核心代碼:

if (s.charAt(i)=='(' || s.charAt(i)=='{' || s.charAt(i)=='[') {
                    stack[top++] = s.charAt(i);
                    }
                 else {
                    if(top == 0) {
                        return false;
                    }
                    char c = stack[top-1];
                    if ('(' == c && ')' == s.charAt(i) ||
                            '{' == c && '}' == s.charAt(i) ||
                            '[' == c && ']' == s.charAt(i)) {
                        --top;
                    }
                    else {
                        return false;
                    }
                }

例2:給定一個大小爲 n 的數組,找到其中的衆數。衆數是指在數組中出現次數大於n/2的元素。
此題不難,一個簡單的思路就是對原數組先排序,然後返回數組中間的那個元素。
但是通過棧可以巧妙的避免排序所導致的時間複雜度過大。由於衆數的數量大於數組元素的一半,所以可以將原數組元素依次進行入棧。入棧的規則爲,棧空直接入,如果數組元素與棧頂元素一樣,直接入棧,如果不一樣數組元素不入棧,棧頂元素出棧。這樣一來最後棧頂元素一定就是衆數。
下面給出此題與此相關的部分核心代碼:

for (int i = 0; i < nums.length; i++) {
            if (top == 0 || stack[top-1] == nums[i]) {
                stack[top] = nums[i];
                top++;
            }
            else {
                top--;
            }
        }

例3:給定字符串 s 和 t ,判斷 s 是否爲 t 的子序列。你可以認爲 s 和 t 中僅包含英文小寫字母。字符串 t 可能會很長(長度 ~= 500,000),而 s 是個短字符串(長度 <=100)。字符串的一個子序列是原始字符串刪除一些(也可以不刪除)字符而不改變剩餘字符相對位置形成的新字符串。(例如,"ace"是"abcde"的一個子序列,而"aec"不是)。
此題的一個巧妙方法是可以用隊列來實現,先把兩字符串都變爲數組,然後把短的字符串放入一個隊列,然後遍歷長數組,並且每次都和對頂元素進行比較,如果一樣,對頂元素出隊列,最後如果隊列爲空那麼s就是t的子序列。
下面給出此題與此相關的部分核心代碼:

for (int i = 0; i < queue2.length; i++) {
            if (top != queue1.length-1 && queue2[i] == queue1[top]) {
                top++;
                continue;
            }
            if (top == queue1.length-1 && queue2[i] == queue1[top]) {
                top++;
                break;
            }
        }

此外,棧和隊列作爲兩種及其重要的數據結構,它們的使用場景還有很多地方,例如棧的先進後出的思想和遞歸算法也是極其相似,因爲遞歸本身就是最先進行遞歸的方法最後再返回。

4.重構數組思想:表面上只是重新再創建一個數組,但是這個可不單單是一個普通的數組,由於我的見識面尚淺,所以暫時只能以計數排序算法思想和歸併排序算法思想爲初始點進行展開。
在計數排序中,我們新創建了兩個數組,第一個數組用來存儲原數組各值的元素數目,進而可以變形爲後面元素表示前面元素之和。第二個數組用於存儲有序的原數組元素,但是這個有序並沒有那麼簡單,第二個數組的元素下標和第一個數組元素有關,而第一個數組的元素下標又與原數組元素有關,這也便是此重構數組的特殊之處。
下面給出計數排序與此相關的部分核心代碼:

        //2.創建統計數組並統計對應元素個數
        int[] countArray = new int[d+1];
        for (int i = 0; i < array.length; i++) {
            countArray[array[i]-min]++;
        }
        //3.統計數組做變形,後面元素等於前面的元素之和
        int sum = 0;
        for (int i = 0; i < countArray.length; i++) {
            sum += countArray[i];
            countArray[i] = sum;
        }
        //4.倒序遍歷原始數組,從統計數組找到正確位置,輸出結果數組
        int[] sortedArray = new int[array.length];
        for (int i = array.length-1; i >= 0; i--) {
            sortedArray[countArray[array[i]-min]-1] = array[i];//-1是因爲sortedArray數組也是從0號下標開始計數
            countArray[array[i]-min]--;
        }

例1:給定一個英文字符串,寫一段代碼找出字符串中首先出現三次的英文字母。
這題並不難,但也用到了上述的重構數組的思想。首先我們得把此字符串轉變爲字符數組,然後我們需要遍歷數組來找到最先出現三次的那個英文字母。此時我們就需要新創建一個數組來用於計數每一種字母出現的次數,而這個新數組的下標恰恰就是原數組中的字母元素。
下面給出此題與此相關的部分核心代碼:

for (int i = 0; i < ch.length; i++) {
            char p = ch[i];
            if (p >= 'a' && p <= 'z' || p >= 'A' && p <= 'Z') {
                hash[p]++;
            }
            if (hash[p] == 3) {
                return p;
            }
        }

例2:給定一個未排序的整數數組,找出其中沒有出現的最小的正整數。你的算法的時間複雜度應爲O(n)。
這個題有一個很巧妙的方法,那便是重構數組。我們先創建一個原數組長度的新數組,然後把原數組中大於1並且小於或等於原數組長度的元素賦值給新的數組,而新數組的下標恰恰就是原數組元素大小-1,此時我們得到的新數組便是一個有序的正數數組(不考慮新數組後面初始化爲0的元素)。
下面給出此題與此相關的部分核心代碼:

 for (int j = 0; j < nums.length; j++) {
            if (nums[j] >= 1 && nums[j] <= nums.length) {
                array[nums[j]-1] = nums[j];//讓新的array數組形成了以下標+1爲基準的排序
            }
        }

在歸併排序的流程中,我們需要把兩段有序的數組合併成一段有序的數組,此時我們這段新的數組就是我們所創建的新數組,而它的獨特之處在於它在被完全賦值之後就已經是一個有序的數組。
下面給出歸併排序與此相關的部分核心代碼:

 while(start <= mid && start2 <= end) {
            if(array[start] <= array[start2]) {
                tmpArray[tmpIndex++] = array[start++];
            }else {
                tmpArray[tmpIndex++] = array[start2++];
            }
        }
        while (start <= mid) {
            tmpArray[tmpIndex++] = array[start++];
        }
        while (start2 <= end) {
            tmpArray[tmpIndex++] = array[start2++];
        }

例3:給定兩個大小爲 m 和 n 的有序數組 nums1 和 nums2。請你找出這兩個有序數組的中位數,並且要求算法的時間複雜度爲 O(log(m + n))。
此題完全可以借用上面歸併排序算法的思想,創建出一個有序的包含此兩原數組元素的新數組,然後返回其中間值即可。
下面給出此題與此相關的部分核心代碼:

while (i <= nums1.length-1 && j <= nums2.length-1) {
           if (nums1[i] <= nums2[j]) {
                array[k++] = nums1[i++];
            }
            else{
                array[k++] = nums2[j++];
            }
        }
        while (i <= nums1.length-1) {
            array[k++] = nums1[i++];
        }
        while (j <= nums2.length-1) {
            array[k++] = nums2[j++];
        }

此外,其實上面的棧和隊列的思想很多時候也是一種重構數組思想,因爲此時我們新創建的棧和隊列也是一個數組。
最後,由於我接觸的題目實在有限,所以暫時也只能想到這麼幾條編程算法思想以及其拓展,但是隨着自己的深入學習,我會相信我還會發現更多的編程算法思想及其拓展。學習貴在堅持和探索!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章