LeetCode初級演算法之字串:387 字串中的第一個唯一字元

木瓜煲雞腳發表於2020-12-08

字串中的第一個唯一字元

題目地址:https://leetcode-cn.com/problems/first-unique-character-in-a-string/

給定一個字串,找到它的第一個不重複的字元,並返回它的索引。如果不存在,則返回 -1。

示例:

s = "leetcode"
返回 0

s = "loveleetcode"
返回 2

提示:你可以假定該字串只包含小寫字母。

解法一:雙指標

雙指標比較,每指定一個值都要全文掃描有無重複

public int firstUniqChar(String s) {
    char[] arr = s.toCharArray();
    int n = arr.length;
    for(int i = 0; i < n; i++){
        boolean flag = true;
        //遍歷每個值與當前值比較,有重複改false
        for(int j = 0; j < n; j++){
            if(i != j && arr[i] == arr[j]){
                flag = false;
            }
        }
        //遍歷完沒有和當前值相同的
        if(flag){
            return i;
        }
    }
    return -1;
}

然後LeetCode的測試用例字串也是真的長(只擷取了部分下面還可以翻頁),所以在n^2的情況下超時。

解法二:細節優化

上面的解法是有可優化的點的。我們去查詢第一個只出現一次的,那麼一個值找到相同的後我們就不必要再往後了遍歷因為不需要看它有幾個相同的,它不滿足就應該看下一個值也就是應該加上break。

for(int j = 0; j < n; j++){
    if(i != j && arr[i] == arr[j]){
        flag = false;
        break;
    }
}

這一點是影響還是比較大的,第二點就是我們去判斷遍歷完了沒有重複也就是找到了可以直接返回的處理我們除了定義flag在迴圈體標記,到迴圈去判斷處理。其實我們去表達迴圈完後的處理也可以在迴圈體裡面,也就是迴圈到最後了仍然不滿足相等。

for(int j = 0; j < n; j++){
    if(i != j && arr[i] == arr[j]){
        break;
    }else if(j == n-1){
        return i;
    }
}

這樣和前面的就是同一個意思,並且少定義一個變數flag,並且最後我們的程式碼不會超時

public int firstUniqChar(String s) {
    char[] arr = s.toCharArray();
    int n = arr.length;
    for(int i = 0; i < n; i++){
        for(int j = 0; j < n; j++){
    	    if(i != j && arr[i] == arr[j]){
       		break;
    	    }else if(j == n - 1){
        	return i;
    	    }
	}
    }
    return -1;
}

時間O(n^2),空間O(1)

解法三:Hash表

那麼使用hash表存資訊,那麼就不用重複遍歷了

public int firstUniqChar(String s) {
    char[] arr = s.toCharArray();
    int n = arr.length;
    HashMap<Character, Integer> count = new HashMap<Character, Integer>();
    //遍歷存下所有資訊
    for (int i = 0; i < n; i++) {
        char c = arr[i];
        count.put(c, count.getOrDefault(c, 0) + 1);
    }
    //遍歷檢視key值為1的
    for (int i = 0; i < n; i++) {
        if (count.get(arr[i]) == 1) 
            return i;
    }
    return -1;
}

時間O(n),空間O(n),雖然如此但效率反而比雙指標要差,這主要是它的這些方法。

解法四:陣列

用Hash表能存,那用陣列也應該是可以的,一樣的key位索引值判斷是不是1。同一個字母就是同一個地方對應值就加一。統計完之後遍歷字串按字串的順序去陣列查率先等於1的就返回

public int firstUniqChar(String s) {
    int[] chars = new int[26];
    char[] arr = s.toCharArray();
    for (char c : arr) {
        chars[c - 'a'] += 1;
    }
    for (int i = 0; i < arr.length; ++i) {
        if (chars[arr[i] - 'a'] == 1) {
            return i;
        }
    }
    return -1;
}

和解法二同樣的一個思路選取陣列這種資料結構,效率就直接上去了。

解法五:細節優化

上述陣列解法在效率上仍然是有可優化點,因為我們去比較兩個容器的時候誰短我們就遍歷誰。更何況這裡只需要拿值到另一個容器參考只需要一次遍歷,那我們更應該遍歷短的。那麼當字串長度小於26和上面一樣遍歷字串到陣列去記錄,最後再遍歷陣列看結果,如果字串長於26那麼我們就遍歷a-z這26個字母

int result = -1;
for (char i = 'a'; i <= 'z'; ++i) {
    int begin = s.indexOf(i);
    int end = s.lastIndexOf(i)
    // 在字串中存在該字元並且唯一
    if (begin != -1 && begin == end {
    	// 不僅要唯一,且索引還要小。遍歷完成拿到字串最前的唯一
    	result = (result == -1 || result > begin) ? begin : result;
    }
}

那麼在字串長度很大的情況下也只需要完整遍歷26次就能找到首個唯一,完整程式碼如下:

public int firstUniqChar(String s) {
    // 字串長度不超過26
    if (s.length() <= 26) {
        int[] chars = new int[26];
        char[] arr = s.toCharArray();
        for (char c : arr) {
            chars[c - 'a'] += 1;
        }
        for (int i = 0; i < arr.length; ++i) {
            if (chars[arr[i] - 'a'] == 1) {
                return i;
            }
        }
    }
    //只遍歷26個字母
    int result = -1;
    for (char i = 'a'; i <= 'z'; ++i) {
        int begin = s.indexOf(i);
    	int end = s.lastIndexOf(i);
    	if (begin != -1 && begin == end) {
            result = (result == -1 || result > begin) ? begin : result;
   	}
    }
    return result;
}

總結

題目難度呢屬於簡單,雙指標、hash表這樣成對的解法就出來了,主要是通過此題去回顧一些注意點比如雙迴圈的優化,迴圈中字串的方法頻繁的進出也是有一定的浪費,可以先拿陣列出來操作會好一點。適合體會解題迭代的一個流程。

相關文章