1. 程式人生 > >演算法分析與設計第十次作業之Remove Duplicate Letters題解

演算法分析與設計第十次作業之Remove Duplicate Letters題解

題解正文

題目描述

在這裡插入圖片描述

問題分析

題目意思是,給定一個只包含小寫字母的字串,我們要刪除其中所有的重複字元,然後從這些刪除方法所得結果中選擇字典序最小的字串作為本題的答案

解題思路

下面的解題過程中我們都是從左往右選擇字元新增到答案字串中

所謂去重,那麼要保證每個字元都要有,我把這個作為破題點,雖然我們最後要獲得字典序最小串,但是前提是滿足要求每個字元都要有; 也就是說每個字元在它最後出現的位置之前一定要選擇它(包括這個最後出現的位置),而在上述最後出現的位置之後出現過的字元則不必出現在該字元前面; 比如字串cbacdcbc,字元d最後出現在位置5,所以在我們遍歷到位置5+1=6之前d字元一定要被選擇(否則由於後續沒有d字元了,那麼就無法保證結果中每個字元都有),而在位置5之後還出現過c、b字元,所以這兩個字元不要求在遍歷到位置6之前就被選擇;

接下來我們將記每個字元最後出現的位置為lastIndex[i],i=字元-‘a’,i∈[0,26)

如果我們考慮lastIndex[i]最小的字元,那麼所有的字元都在該lastIndex[i]之後(包括此位置)出現過,每一個字元j+'a'都在lastIndex[j]出現過,因為lastIndex[j] >= lastIndex[i],j∈[0,26),所以字元j+'a'會出現在i+'a'之後至少一次; 再結合上面第三段的分析,所有非i+'a'的字元都不要求在lastIndex[i]這個位置之前(包括這個位置)被選擇,因為它們在後面還有機會被選到; 那麼接下來就可以在lastIndex[i]位置之前進行任意選擇了,那麼自然我們要選擇最小的字元加入答案,這樣才能保證得到字典序最小且滿足要求的字串,並且我們只有這樣做選擇才能得到結果:設想我們選擇了其它更大的字元加入到答案字串中,那麼得到的答案字串字典序一定比前一種做法得到的答案字串字典序大,因為當前選擇的字元是第一個字元,具有最高優先順序,第一位字元更小的字串字典序一定更小,不用管其它位,比如azzzzz比zaaaaa小就是這個道理; 當我們完成了第一位字元的選擇之後,將指標移動到該字元位置+1(該位置之前的字元不再考慮以保證有序),就將isVisited[i]設為true(後續不再考慮該字元以防重複出現),設定lastIndex[i]為∞(表示該字元不存在於當前字串中),再回到前面第四段的步驟,重複操作,不斷選出當前字串中符合要求的最小字元(即符合下標在lastIndex[i]之前的),直到所有字元都被選過/指標移動超出字串長度,表示所有出現過的字元都被選過了,演算法結束;

下面簡單說明一下演算法正確性:如果演算法保證所有字元在答案中出現且僅出現一次、且所得結果是所有去重方法得到結果中字典序最小的,那麼就說明這個演算法正確;

  • 所有字元在答案中出現且僅出現一次: 每次選擇一個字元標記isVisited[i]設為true,保證每個字元只出現一次; 每次選擇一個字元將指標移動到該字元位置+1,保證結果字串在保留源字串中的順序; 所有字元都被選過/指標移動超出字串長度時結束演算法保證所有字元都出現過;
  • 所得結果是所有去重方法得到結果中字典序最小的: 上面第六段我們已經分析過了,每次選擇都是選擇當前字串中滿足要求的最小字元(即符合下標在lastIndex[i]之前的),如果超過下標lastIndex[i]那麼不符合要求(在後面的字元中沒法選出字元 i+'a'
    ,少一個字元不符合要求),滿足下標情況下所選擇的字元都是最優的,因此能得到最優解。

演算法步驟

  • 方法1:遞迴方法
    removeDuplicates(string s) 函式,輸入字串s,返回結果string
    	如果字串s為空,返回空串;
    	遍歷字串s,初始化lastIndex陣列;
    	遍歷lastIndex陣列找出最小的lastIndex[i];
    	再次遍歷字串s,下標從0到lastIndex[i],找出該範圍最小的字元記為c;
    	遍歷字串s,刪除串中所有的c字元以及第一個c字元前面的所有字元;
    	返回字串result=c+removeDuplicates(s),因為c是我們找到的第一個符合條件的字元,而遞迴呼叫removeDuplicates(s)能夠從剩下字串中找出符合條件的字串,所以加起來就是結果,當我們不斷找出滿足條件的第一個字元直到剩餘字串為空,我們將得到答案;
    
  • 方法2:迭代方法
    removeDuplicates(string s) 函式,輸入字串s,返回結果string
    	遍歷字串s,初始化lastIndex陣列(不存在的字元其lastIndex值為∞);
    	初始化用於表示當前位置的下標指標cur=0(在該下標之前的字元不再考慮,以保證有序性);
    	外層迴圈找出答案字串,直到下標指標超過s長度(單次迴圈找出一個符合答案要求的字元):
    		遍歷lastIndex陣列找出當前最小的lastIndex[i],如果最小的lastIndex[i]==∞,說明當前串為空,結束演算法;
    		遍歷字串s,下標從0到lastIndex[i],找出該範圍isVisited為false(即當前答案串還沒有該字元)的最小的字元記為c;
    		將c加入答案字串res的末尾;
    		將cur更新為c_pos+1,下個字元找尋從這次選出的字元c之後找;
    		將lastIndex[s[cur-1]-'a']更新為∞,表示字串中不存在該字元;
    		將isVisited[s[cur-1]-'a']更新為1,表示該字元已經有了,後續尋找字元不再考慮;
    	外層迴圈結束,已找出答案字串res,將其返回;
    

複雜度分析

  • 方法1:遞迴方法 因為一共26個不同字元,每次遞迴找出一個符合答案的字元並且遞迴複雜度不超過O(2n),所以總體複雜度是O(26*2n)=O(n); 每一次遞迴都是2次遍歷當前字串(兩個for迴圈),並且可能中途break,所以其複雜度不超過O(2n);
  • 方法2:迭代方法 同理,一共26個不同字元,外層迴圈每個單次迴圈都找出一個符合答案的字元,所以外層最多迴圈26次; 內層迴圈遍歷當前字串(一個for迴圈),並且可能中途break,所以其複雜度不超過O(n); 所以總體複雜度是O(26*n)=O(26n)

程式碼實現&結果分析

  • 方法1:遞迴方法 程式碼實現:
    class Solution {
    public:
        string removeDuplicateLetters(string s) {
        	int length = s.length();
        	int charCount[26] = {};
        	int minCharIndex = 0;
        	for (int i = 0; i < length; ++i) {
        		charCount[s[i]-'a']++;
        	}
        	for (int i = 0; i < length; ++i) {
        		if (s[i] < s[minCharIndex]) minCharIndex = i;
        		charCount[s[i]-'a']--;
        		if (charCount[s[i]-'a'] == 0) break;
        	}
    
        	string temp = "";
        	for (int i = minCharIndex+1; i < length; ++i) {
        		if (s[i] != s[minCharIndex]) {
        			temp.append(1,s[i]);
        		}
        	}
    
        	return length == 0 ? "" : (s[minCharIndex] + removeDuplicateLetters(temp));
        }
    };
    
    提交結果: 在這裡插入圖片描述 這個結果並不是很好,估計是遞迴呼叫花費了很多開銷用於儲存現場(區域性變數、函式引數等),後面使用迭代方法實現,演算法是一毛一樣的,但是runtime只有一半,所以應該來說都是遞迴呼叫的鍋而不是演算法的問題。
  • 方法2:迭代方法
    class Solution {
    public:
    	int minIndex (int* lastIndex) {
    		int index = -1;
    		int min = 1000000;
    		for (int i = 0; i < 26; ++i) {
    			if (lastIndex[i] != -1 && min > lastIndex[i]) {
    				min = lastIndex[i];
    				index = i;
    			}
    		}
    		return index;
    	}
    
        string removeDuplicateLetters(string s) {
        	string res = "";
        	int isVisited[26] = {};
        	int lastIndex[26] = {};
        	for (int i = 0; i < 26; ++i) {
        		lastIndex[i] = -1;
        	}
    
        	int length = s.length();
        	for (int i = 0; i < length; ++i) {
        		lastIndex[s[i]-'a'] = i; 
        	}
    
        	int cur = 0;
        	while (cur < length) {
        		int minI = minIndex(lastIndex);
    			if (minI == -1) break;
        		char minTemp = 'z'+1;
        		for (int i = cur; i <= lastIndex[minI]; ++i) {
        			if (isVisited[s[i]-'a'] == 0 && minTemp > s[i]) {
        				minTemp = s[i];
        				cur = i+1;
        			}
         		}
         		res.append(1, minTemp);
         		lastIndex[s[cur-1]-'a'] = -1;
         		isVisited[s[cur-1]-'a'] = 1;
        	}
        	return res;
        }
    };
    
    提交結果: 在這裡插入圖片描述 迭代方法明顯優於遞迴方法,得益於節省了儲存現場的開銷,雖然演算法思想一樣還是節省了一半時間(在這個題目裡)

心得體會

本週一開始做了幾個動態規劃的題目都覺得不算很難,都是常規的動態規劃題嘛,然後直到遇到Create Maximum Number這個題,然後讓我徹底自閉,這個題目寫到心態爆炸,寫leetcode到目前為止第一次自閉,為什麼這麼說呢,這個題拿到手就沒法下手(也有可能週六剛好狀態很差),然後想了很久都沒辦法做到一個比較好的複雜度(自己的辦法超出多項式複雜度),瞄了一眼評論區基本上都是O(KN)複雜度的,但是自己又想不到什麼更好的辦法,所以只能求助評論區,然後發現這個題目可以分兩個步驟,然後第一個步驟是leet code上另一道標籤為hard的難題Remove Duplicate Letters的變式,所以我決定先嚐試做這個子問題,看看能否有什麼幫助,結果就是連這個子問題自己都花費了很長時間,雖然子問題解決之後Create Maximum Number就有辦法做到O(KN)複雜度了,因為這周還有各種其它作業,所以沒辦法再完成Create Maximum Number,所以決定做一篇Remove Duplicate Letters的題解部落格,後續再完成Create Maximum Number的部落格(先立flag)。 這次遇到這個題目自己認栽。。。。。