1. 程式人生 > >找出給定字串中第一個不重複的字元

找出給定字串中第一個不重複的字元

給定一串字串,找出其中第一個不重複的字元。
如:輸入”abcddcaeb1~soop”,輸出’e’

方法一

思路:
定義list<char> storelist<char> storeDel,對輸入字串str進行遍歷,對str的每一個字元,分別在store和storeDel中查詢,如果在store中存在該字元,則把store中的該字元刪除,並存入storeDel中(如果storeDel中已經存在該字元,則不需要再存入),如果store和storeDel都沒有該字元,則將該字元放進store中。
即用store存放字串中只出現了一次的字元,而storeDel儲存出現了多次的字元。那麼當遍歷完str後,store的第一個元素就是我們要找的字元,將之返回即可。

程式碼:

#include <iostream>
#include <map>
#include <algorithm>
#include <list>

using namespace std;
char findFirstCh(string str){
       list<char> store;//儲存只出現一次的字元
       list<char> storeDel;//儲存重複出現的字元
       for(unsigned int i = 0; i < str.size(); ++i){
               list
<char>
::iterator litor = find(store.begin(), store.end(), str[i]);//查詢list裡是否已經存在當前字元 list<char>::iterator litorDel = find(storeDel.begin(), storeDel.end(), str[i]);//還要查詢已經出現過的重複字元 if(litor != store.end()){//如果存在,說明該字元在字串裡是重複的,把字元從list裡刪除 store.remove(str[i]); if
(litorDel == storeDel.end()){//記錄重複的字元 storeDel.push_back(str[i]); } } else{//list不存在該字元,當前字元暫時符合要求 if(litorDel == storeDel.end())//重複的字元裡沒有當前字元,符合要求,存入store store.push_back(str[i]); } } char ch = -1; list<char>::iterator it = store.begin(); if(it != store.end()){//如果list的第一個元素存在,就是我們要找的字元 ch = *it; return ch; } return -1;//不存在,返回-1 } int main() { string s = "bbccdfeesbsbdfh1h"; string s1 = "ababcdefghscdfe"; char ch = findFirstCh(s1); if(ch != -1) cout << ch <<endl; else cout << "not match character" << endl; }

分析:
這種方法用vector也可以實現,但是注意到遍歷字串會發生push_back和remove的操作,vector是用陣列實現的,在陣列某個位置刪除元素(除了最後一個元素),後面的元素都要往前移;而進行push_back操作,當vector預分配的大小不足時,會開闢一塊更大的記憶體,將原來記憶體的元素複製到新的記憶體。
而list的底層是利用一個雙向環形連結串列實現,對連結串列的某個元素進行刪除,不會造成後續元素在記憶體中的移動,也不存在預分配大小不足的問題。

空間複雜度:
定義了兩個list,list的大小隨著字串長度的增加也會相應地增加。

時間複雜度:
list是連結串列,隨機查詢某個元素的時間複雜度為O(n);同理,對list進行remove操作,也要先找到字元所在的位置,因此這個演算法的時間複雜度為O(n^2)。

總結:
時間複雜度和空間複雜度都比較大,必須要遍歷完整個字串才能確定符合要求的字元。

方法二

思路:
遍歷待查詢字串,對每個字元,分別查詢字串中是否還存在相同字元,如果不存在,則說明已經找到我們要查詢的字元,可以返回;如果存在,說明不符合要求,繼續對後面的字元進行相同操作。

程式碼:

#include <iostream>
#include <map>
#include <algorithm>
#include <list>

using namespace std;

char findFirstCh(string str){
    for(string::iterator iter = str.begin(); iter != str.end(); ++iter){
        char ch = *iter;
        string::iterator fitor1 = find(str.begin(), iter, ch);//查詢字串開頭到當前字元中間是否存在和當前字元相同的字元(當前字元所在的位置不在查詢範圍之內)
        string::iterator fitor2 = find(iter+1, str.end(), ch);//從當前字元後一個位置開始到字串結尾,查詢是否存在和當前字元一樣的字元,注意,str.end()並不是最後一個元素,而是最後一個元素後的一個位置
        if(fitor1 == iter && fitor2 == str.end()){//如果兩次查詢都失敗,那麼證明當前這個字元在整個串只出現一次,就是我們要找的字元,馬上返回
            return ch;
        }   
    }
    return -1;
}

int main() {
    string s = "bbczcdfeesbs1bdfh1h";
    string s1 = "ababcdefghscdfeg";
    char ch = findFirstCh(s);
    if(ch != -1)
        cout << ch <<endl;
    else
        cout << "not match character" << endl;
}

分析:
這個方法,關鍵在於查詢的時候,要把待查詢字串分成兩部分,即進行兩次find操作,如果對整個字串進行查詢,因為字元本身就屬於字串,返回的結果肯定是存在的。而find操作中兩個迭代器引數,正好是半開半閉區間,即查詢範圍是[a,b),b不在查詢範圍內。所以這種方法中對字串的遍歷要使用迭代器,兩次查詢的分界正好就是指向當前字串的迭代器。

空間複雜度:
只需要一個char記錄返回值,O(1),與字串長度無關。

時間複雜度:
最壞的情況下,要查詢的字元位於字串的最後一位,那麼查詢時間將是O(n^2)。

總結:
相比第一種方法,方法二不需要定義額外的資料結構,而且在迴圈中第一個符合要求的字元就是我們要查詢的元素,可以馬上返回,不需要對剩下的字元進行遍歷,這也比第一種方法有了改進。當然,像上面所說,最壞的情況下,這種方法的時間複雜度還是達到了O(n^2)。

方法三

思路:
在遍歷待查詢字串的情況下,對list或者string進行find操作,最壞情況下時間複雜度為O(n^2)。
考慮使用map,查到的資料提到,由於map底層用紅黑樹實現,查詢的時間複雜度為O(logn)。因此可以定義map<char,int>,遍歷字串時在map查詢當前字元,如果存在,將second置為0,如果不存在將second置為1。那麼當遍歷完字串時,這個map裡second為1的char就代表只出現了一次的字元。構造完map後,再次遍歷字串,利用字元查詢map中對應second值(value值),第一個遇到value為1的字元就是要查詢的字元。

程式碼:

#include <iostream>
#include <map>
#include <algorithm>
#include <list>

using namespace std;

char findFirstCh(string str){
    map<char,int> res;
    for(unsigned int i = 0; i < str.size(); ++i){
            map<char,int>::iterator itor = res.find(str[i]);
            if(itor != res.end()){//res中存在和當前字元相同的字元
                res[str[i]] = 0;//將對應的value置為0
            }
            else{//res中不存在和當前字元一樣的字元
                res[str[i]] = 1;//將對應的value置為1
            }
    }//迴圈執行完畢後,second為1表示對應key在字串中只出現一次,second為0的key表示在字串中出現了多次


    for(unsigned int i = 0; i < str.size(); ++i){
        if(res[str[i]] == 1)
            return str;
    }

    return -1;
}

int main() {
        string s = "bbccdfeesbsbdfh1h";
        string s1 = "ababcdefghscdfe";
        char ch = findFirstCh(s1);
        if(ch != -1)
            cout << ch <<endl;
        else
            cout << "not match character" << endl;  
}

分析:
這個方法相比前兩種方法,主要是在map中進行find操作,時間複雜度有所改進。
這裡容易犯一個錯誤,就是在構造完map後,查詢map的第一個second為1的key,將其作為結果返回。
其實map的存取順序是不一致的,即當我們按自己的順序把元素存放進map後,重新讀取,map的元素已經不是我們儲存的順序了,而是按照key進行了排序。
比如把key為b和a的元素先後存入map,那麼讀取到map的第一個資料將會是key為a那個。所以如果查詢map中第一個second為1的元素,是無法達到效果的。舉個例子:”aabbcfec”,按照題目要求,應該輸出’f’,如果遍歷查詢map中第一個second為1的元素,那麼就會輸出’e’。所以建立map後,這裡要遍歷的是待查詢字串,用每一個字元作為key去獲取map中對應的value(即second)的值,第一個second為1的字元才是正確結果。

空間複雜度:
定義了map<char,int>結構,map佔用記憶體和字串包含不同字元個數相關,不同字元個數越多,map越大。

時間複雜度:
第一次遍歷必須遍歷完整個字串,而每次進行查詢時間複雜度為O(logn),因此第一個迴圈時間複雜度為O(nlogn)。第二個迴圈最壞情況下時間複雜度為O(n)。因此整個演算法時間複雜度為O(nlogn+n)。

總結:
如果對map的儲存方式不清楚,會誤以為map中元素的存取資料是按照使用者寫入順序儲存,那麼每次輸出的將會是字串中所有不重複字元裡字典順序最小的字元。

方法四

思路:
建立雜湊表char hashTable[256],陣列元素初始化為0,雜湊函式

void hashFunc(char ch){
    hashTable[ch]++;//在雜湊表的儲存位置就是ch對應的ascii碼,
}

構造完雜湊表後,表中每個元素的值就代表陣列下標對應的字元在字串中出現的次數,再次遍歷字串,以每個字元對應的ascii碼錶作為下標訪問雜湊表對應的元素,第一次遇到元素值為1,即表示當前字元就是要查詢的字元。

程式碼:

#include <iostream>
#include <map>
#include <algorithm>
#include <list>

using namespace std;

char findFirstCh(string str){
    char store[256] = {0};
    for(unsigned int i = 0; i < str.size(); ++i){
        store[str[i]]++;
    }

    for(unsigned int i = 0; i < str.size(); ++i){
        if(store[str[i]] == 1)
        return str[i];
    }

    return -1;
}

int main() {
        string s = "[email protected]@qpnmmno";
        string s1 = "aba";
        char ch = findFirstCh(s);
        if(ch != -1)
            cout << ch << endl;
        else
            cout << "not match character" << endl;
}

分析:
其實這個所謂的雜湊表就相當於ascii碼錶,只是它還多出了一個功能,記錄每次字元出現的次數。
這裡面會容易進入誤區,就是建立完雜湊表後,從雜湊表開始遍歷,輸出遇到的第一個為1的元素對應的字元。因為題目要求是找出第一個不重複的字元,而雜湊表中第一個為1的元素對應的字元不一定能就滿足條件,只能保證是不重複的字元裡字典順序最小的字元。
這和第三種方法的易犯錯誤其實是一個道理。

空間複雜度:
陣列char[256]作為雜湊表,256是ascii碼錶的大小,與字串長度無關,即空間複雜度為常量。

時間複雜度:
對字串進行兩次遍歷操作,時間複雜度為O(n)。

總結:
這種方法其實和第三種方法大同小異,思路基本一致,只是第三種方法構造雜湊表的過程交給map去做而已。當然,第四種方法在時間複雜度上無疑更有優勢。