1. 程式人生 > >雜湊表演算法面試題

雜湊表演算法面試題


分析

最常規的演算法當然是先對陣列進行排序,然後從兩端開始逐漸調整下標使得兩個元素的和為目標值。但是,題目要求返回資料的下標,因此我們只能在陣列的拷貝上進行排序。空間複雜度O(n),時間複雜度O(n log )。

我們只需要找到特定的一組元素,而我們對整個陣列進行了排序,是否有這個必要呢?

我們可以維護這樣一個雜湊表<元素,元素下標>,如果說target-a[i]也存在於雜湊表中,那麼我們就得到了這樣的一對元素。

注:需要注意相同元素的情況。

public class TwoSum { 
    public int[] twoSum(int[] nums, int target) {
    	int[] res=new int[2]; 
    	Map<Integer,Integer> indexMap=new HashMap<Integer,Integer>();
    	for(int i=0;i<nums.length;i++){
    		if(target%2==0&&nums[i]==target/2&&indexMap.get(target/2)!=null){
    			int secondIndex=indexMap.get(target/2);
    			res[0]=Math.min(i, secondIndex);
    			res[1]=Math.max(i, secondIndex);
    			return res;
    		}
    		indexMap.put(nums[i], i);
    	} 
    	for(int i=0;i<nums.length;i++){
    		if(indexMap.containsKey(target-nums[i])&&indexMap.get(target-nums[i])!=i){
    			int secondIndex=indexMap.get(target-nums[i]);
    			res[0]=Math.min(i, secondIndex);
    			res[1]=Math.max(i, secondIndex);
    			break;
    		}
    	}
		return res; 
    }
}

分析

對於數獨的驗證,我們需要驗證行,驗證列,還需要驗證9個3*3的格子。

因為每一行、列、3*3的格子只需要驗證一次。並且是存在性判斷,對於存在性判斷(重複),我們必定會想到雜湊表。

public class ValidSudoku {

    public boolean isValidSudoku(char[][] board) {
        int[][] rowsMap=new int[9][9];
        int[][] colsMap=new int[9][9];
        int[][] gridsMap=new int[9][9];
        for(int row=0;row<9;row++){
        	for(int col=0;col<9;col++){
        		if(board[row][col]=='.') continue;
        		int value=board[row][col]-'1';//1-9變換成0~8
        		//驗證行
        		if(rowsMap[row][value]==1){
        			return false;
        		}else{
        			rowsMap[row][value]=1;
        		}
        		//驗證列
        		if(colsMap[col][value]==1){
        			return false;
        		}else{
        			colsMap[col][value]=1;
        		}
        		//驗證單元格
        		int index=(row/3)*3+col/3;
        		if(gridsMap[index][value]==1){
        			return false;
        		}else{
        			gridsMap[index][value]=1;
        		}
        	}
        }
        return true;
    }

}

分析

從前往後遍歷,我們用一個256位的陣列(雜湊表)記錄每個字元上一次出現的位置,如果當前元素s[i]和t[i]相等,且上一次出現的位置也一樣,則繼續遍歷,否則返回false。

public class Solution {
    public boolean isIsomorphic(String s, String t) { 
    	if(s.length()!=t.length()) return false;
    	int[] m1=new int[256];
    	int[] m2=new int[256];
        int  n = s.length();
        for(int i=0;i<256;i++){
        	m1[i]=m2[i]=-1;
        }
        for (int i = 0; i < n; ++i) {
            if (m1[s.charAt(i)] != m2[t.charAt(i)]) return false;
            m1[s.charAt(i)] = i ;
            m2[t.charAt(i)] = i ;
        }
        return true; 
    }
}

public class Solution {
    public boolean containsDuplicate(int[] nums) {
    	HashMap<Integer,Integer> map=new HashMap<Integer,Integer>();
    	for(int i=0;i<nums.length;i++){
    		if(map.get(nums[i])!=null){
    			return true;
    		}else{
    			map.put(nums[i], 1);
    		}
    	}
		return false; 
    }
}

分析:

在S的子串S[begin,end-1]中,我們確保其中的元素不重複。當我們向其中新增元素S[end],如果S[end]不在子串中,將其加入子串中,子串變成S[begin,end]。如果在子串中存在S[pre]與S[end]重複,那麼更新子串為S[pre+1,end+1],即將之前重複的元素淘汰,新的元素加入。

對於元素的存在性問題雜湊表再合適不過了,其查詢為O(1),但是我們不僅僅是要存在性判斷,我們還需要記錄其上一次出現的下表,演算法如下:

public class Solution {
    public int lengthOfLongestSubstring(String s) {
    	if(s==null||s.length()==0)return 0;
    	int res=1,begin=0,end=1;
    	HashMap<Character,Integer> indexs=new HashMap<Character,Integer>(); 
    	indexs.put(s.charAt(0), 0);
    	while(end<s.length()){ 
    		Integer preIndex=indexs.get(s.charAt(end)); 
    		if(preIndex==null){
    			indexs.put(s.charAt(end), end); 
    			res=Math.max(res, indexs.size()); 
    		}else{
    			while(begin<=preIndex){ 
    				indexs.remove(s.charAt(begin++)); 
    			}
    			indexs.put(s.charAt(end), end);
    		}
    		end++;
    	} 
		return res; 
    } 
}

分析:

利用雜湊表統計各字元的數量,數量相同就分到同一組。

public class Solution {
    public List<List<String>> groupAnagrams(String[] strs) {
    	List<List<String>> res=new ArrayList<List<String>>();
    	HashMap<List<Integer>,List<String>> map=new HashMap<List<Integer>,List<String>>();
    	for(String str:strs){
    		ArrayList<Integer> counts=new ArrayList<Integer>(Collections.nCopies(26, new Integer(0)));
    		for(char c:str.toCharArray()){ 
    			counts.set(c-'a', counts.get(c-'a')+1);
    		}
    		if(map.get(counts)==null){
    			List<String> list=new ArrayList<String>();
    			list.add(str);
    			map.put(counts, list);
    		}else{
    			List<String> list=map.get(counts);
    			list.add(str);
    		}
    	}
    	for(List<String> value:map.values()){
    		res.add(value);
    	}
		return res; 
    }
}

分析:

方案一:首先我們可以利用HashMap統計所有元素出現的次數,出現兩次時就移除,最後剩下的元素就是我們要找的元素。

    public int singleNumber(int[] nums) {
    	HashMap<Integer,Integer> countMap=new HashMap<Integer,Integer>();
    	for(int i:nums){
    		if(countMap.get(i)==null){
    			countMap.put(i, 1);
    		}else{
    			countMap.remove(i);
    		}
    	}
    	int res=0;
    	for(Integer key:countMap.keySet()){
    		res=key;
    		break;
    	} 
    	return res;
    }

方案二:我們可以將所有元素異或,最後的結果就是該元素。

public class Solution {
    public int singleNumber(int[] nums) {
    	int res=0;
    	for(int i:nums){
    		res^=i;
    	}
    	return res;
    }
}

方案三:根據異或的原理,我們記錄2^i次方出現的次數,0<=i<=31(int四位元組,32位),最後我們將出現奇數次的位置1,出現偶數次的位置零,我們就得到的最終結果的二進位制表示,最後根據二進位制表示轉換成整數即可。

public class Solution {
    public int singleNumber(int[] nums) {
    	int[] count=new int[32]; 
    	for(int i:nums){
    		int location=0,t=1;
    		while(location<=31){ 
    			if((t&i)!=0){
    				count[location]=(count[location]+1)%2;
    			}
    			location++;
    			t<<=1;
    		} 
    	} 
    	int sign=1;
    	if(count[31]==1){//負數,將補碼轉換成原碼
    	    sign=-1;//記錄結果的符號
    	    //0和1轉換
        	for(int i=31;i>=0;i--){
        		if(count[i]==0){
        			count[i]=1;
        		}else{
        			count[i]=0;
        		}
        	}
        	//+1
        	int add=1;
        	for(int i=0;i<=31;i++){ 
        		int sum=count[i]+add;
        		count[i]=sum%2;
        		add=sum/2;
        		if(add==0) break;
        	}
    	}
    	StringBuilder builder=new StringBuilder(); 
    	for(int i=31;i>=0;i--){//從高位到低位 
    	    builder.append(count[i]);
    	}
    	return Integer.valueOf(builder.toString(), 2)*sign; 
    }
}
擴充套件:

變形1:除了一個數出現一次外,其餘都出現3次呢?第二種方案肯定不能解決問題,但是方案三稍做改變就可以適用。

變形2:如果我們除了兩個數(A和B)出現一次外,其餘都出現兩次呢?顯然似乎只有方案一可行了,是這樣嗎?如果我們將所有的整數異,結果的二進位制位表示中至少有一個位是1(因為A!=B),我們將所有整數按照二進位制表示中該位是否為1的規則將整數劃分為兩個部分,然後分別進行異或運算。這樣,我們就通過方案二得到了正確的結果。

public class Solution {
    public List<String> findRepeatedDnaSequences(String s) {
    	List<String> res=new ArrayList<String>();
    	HashMap<String,Integer> map=new HashMap<String,Integer> ();
    	for(int i=0;i<=s.length()-10;i++){
    		String t=s.substring(i, i+10);
    		if(map.get(t)==null){
    			map.put(t, 1);
    		}else{
    			if(map.get(t)==1){
    				res.add(t);
    			}
    			map.put(t, map.get(t)+1);
    		}
    	}
		return res; 
    }
}

分析:

與最長不重複子串思路類似(Leetcode 3),只不過我們這裡的元素允許重複,我們記錄元素出現的次數,並且需要記錄元素出現的列表。

   public List<Integer> findSubstring(String s, String[] words) {
    	List<Integer> res=new ArrayList<Integer>();
    	HashMap<String,Integer> countMap=new HashMap<String,Integer> ();
    	for(String word:words){//統計每個單詞出現的次數
    		if(countMap.get(word)==null){
    			countMap.put(word, 1);
    		}else{
    			countMap.put(word, 1+countMap.get(word));
    		} 
    	}
    	//匹配到的單詞的下標
    	HashMap<String,List<Integer>> findMap=new HashMap<String,List<Integer>> ();//匹配元素
    	int wl=words[0].length(),//單詞長度
    			wc=words.length;//單詞個數
    	for(int i=0;i<wl;i++){
    	    findMap.clear();
    		int begin=i,//記錄起始點
    				now=i;//當前匹配點
    		while(now+wl<=s.length()){
    			String t=s.substring(now, now+wl);
    			if(!countMap.containsKey(t)){//單詞不匹配,重新匹配
    				findMap.clear();
    				begin=now+wl;//更新起點
    			}else{
    				if(!findMap.containsKey(t)){//之前沒找到過
    					List<Integer> list=new LinkedList<Integer>();
    					list.add(now);
    					findMap.put(t, list);
    					if((now-begin)==(wl*wc-wl)){//符合要求
    						res.add(begin);
    					}
    				}else{
    					if(findMap.get(t).size()<countMap.get(t)){//還可以匹配t
    						findMap.get(t).add(now);
        					if((now-begin)==(wl*wc-wl)){//符合要求
        						res.add(begin);
        					}
    					}else{//多餘字元t
    						int first=findMap.get(t).get(0);//t第一次出現的位置
    						while(begin<=first){
    							findMap.get(s.substring(begin, begin+wl)).remove(0);
    							begin+=wl;
    						}
    						findMap.get(t).add(now);
        					if((now-begin)==(wl*wc-wl)){//符合要求
        						res.add(begin);
        					}
    					}
    				}
    			}
    			now+=wl;
    		}
    	}
		return res; 
    }


分析

對於數獨的解法,我們都知道是通過回溯,每個待填寫的格子我們都需要嘗試各種可能性,但是我們在嘗試時必須判定合法性(行、列或3*3格子中是否有重複),而不是填入後再驗證合法性,因此我們可以利用雜湊表來進行存在性驗證,簡化合法性驗證過程。

public class Solution {
	boolean[][] rowMaps=new boolean[9][9];
	boolean[][] colMaps=new boolean[9][9];
	boolean[][] cellMaps=new boolean[9][9]; 
	private void mark(int row,int col,char c){
		int index=c-'1';
		rowMaps[row][index]=true;
		colMaps[col][index]=true;
		cellMaps[row/3*3+col/3][index]=true;
	}
	private void unMark(int row,int col,char c){
		int index=c-'1';
		rowMaps[row][index]=false;
		colMaps[col][index]=false;
		cellMaps[row/3*3+col/3][index]=false;
	}
	private boolean isValid(int row ,int col,char c){
		int index=c-'1';
		return rowMaps[row][index]==false&&
				colMaps[col][index]==false&&
				cellMaps[row/3*3+col/3][index]==false;
	}
    public void solveSudoku(char[][] board) {
        //初始化
    	ArrayList<Integer> locations=new ArrayList<Integer>();
    	for(int row=0;row<9;row++){
    		for(int col=0;col<9;col++){
    			char c=board[row][col];
    			if(c=='.'){
    			    locations.add(row*9+col);
    				continue;
    			}else{
    				mark(row,col,c);
    				
    			}
    		}
    	}
    	int now=0;
    	while(now<locations.size()&&now>=0){
    		int location=locations.get(now);
    		int row=location/9,col=location%9;
    		char next='1',c=board[row][col];
    		if(c!='.'){
    			next=(char)(c+1);
    			unMark(row,col,c);
    		}
    		while(next<='9'){ //找到下一個合法的值
    			if(isValid(row,col,next)){
    				break;
    			} 
    			next++;
    		}
    		if(next=='9'+1){//嘗試完了都
    			now--;
    			board[row][col]='.';
    		}else{
    			board[row][col]=next;
    			mark(row,col,next);
    			now++;
    		}
    	}  
    }
}

public class Solution {
	private boolean isValid(HashMap<Character,Integer> countMap,HashMap<Character,Integer> findMap){
		for(Character c:countMap.keySet()){
			if(findMap.get(c)==null||findMap.get(c)<countMap.get(c)){
				return false;
			}
		}
		return true;
	}
    public String minWindow(String s, String t) { 
    	int minWindow=Integer.MAX_VALUE,start=-1;
    	LinkedList<Integer> indexs=new LinkedList<Integer>();
    	HashMap<Character,Integer> countMap=new HashMap<Character,Integer>();
    	HashMap<Character,Integer> findMap=new HashMap<Character,Integer>();
    	//初始化
    	for(char c:t.toCharArray()){ 
    		countMap.put(c, 0);
    		findMap.put(c, 0);
    	}
    	for(char c:t.toCharArray()){
    		countMap.put(c, countMap.get(c)+1);
    	}
    	int now=0;
    	while(now<s.length()){
    		char c=s.charAt(now);
    		if(!countMap.containsKey(c)){
    			now++;
    			continue;
    		}else{//存在於t中
    			indexs.add(now);
    			if(findMap.get(c)<countMap.get(c)){//還可以匹配該字元
    				findMap.put(c,findMap.get(c)+1);
    				while(isValid(countMap,findMap)){//匹配
    					if(indexs.getLast()-indexs.getFirst()+1<minWindow){//更新最小窗體大小
    						minWindow=indexs.getLast()-indexs.getFirst()+1;
    						start=indexs.getFirst();
    					}
    					//使得findMap達到不匹配狀態
    					char first=s.charAt(indexs.removeFirst());
    					findMap.put(first, findMap.get(first)-1);
    				}
    			}else{//超出特定數量,肯定不會匹配
    				findMap.put(c, findMap.get(c)+1);
    			} 
    		}
    		now++;
    	}
    	if(start==-1){
    		return "";
    	}else{
    		return s.substring(start,start+minWindow);
    	} 
    }
}

優化:

我們可以用陣列來實現雜湊表而不是用HashMap,效率更高。此外,我們用一個counter記錄已經匹配個數,而不是每次進行檢查是否完成匹配。

public class Solution {
    public String minWindow(String s, String t) {
    	int[] countMap=new int[128];
    	int[] findMap=new int[128];
    	for(char c:t.toCharArray()){
    		countMap[c]++; 
    	}
    	int counter=0,begin=0,end=0,head=0,minWindow=Integer.MAX_VALUE;
    	while(end<s.length()){
    		char c=s.charAt(end);
    		if(countMap[c]==0){//不在t中的字元,忽略掉
    			end++;
    			continue;
    		}else{
    			findMap[c]++;
    			if(findMap[c]<=countMap[c])counter++;//匹配字元
    			end++;
    			while(counter==t.length()){//匹配 
    				if(end-begin<minWindow){
    					head=begin;
    					minWindow=end-begin;
    				}
    				if(countMap[s.charAt(begin)]==0){//不存在於t
    					begin++;
    				}else{
    					findMap[s.charAt(begin)]--;
    					if(findMap[s.charAt(begin)]<countMap[s.charAt(begin)]) counter--;//消除匹配,保證處於不匹配的狀態
    					begin++; 
    				}
    			} 
    		}
    	}
    	
		return minWindow==Integer.MAX_VALUE?"":s.substring(head, head+minWindow); 
     
    }
}


方法論:

I will first give the solution then show you the magic template.

The code of solving this problem is below. It might be the shortest among all solutions provided in Discuss.

string minWindow(string s, string t) {
        vector<int> map(128,0);
        for(auto c: t) map[c]++;
        int counter=t.size(), begin=0, end=0, d=INT_MAX, head=0;
        while(end<s.size()){
            if(map[s[end++]]-->0) counter--; //in t
            while(counter==0){ //valid
                if(end-begin<d)  d=end-(head=begin);
                if(map[s[begin++]]++==0) counter++;  //make it invalid
            }  
        }
        return d==INT_MAX? "":s.substr(head, d);
    }

Here comes the template.

For most substring problem, we are given a string and need to find a substring of it which satisfy some restrictions. A general way is to use a hashmap assisted with two pointers. The template is given below.

int findSubstring(string s){
        vector<int> map(128,0);
        int counter; // check whether the substring is valid
        int begin=0, end=0; //two pointers, one point to tail and one  head
        int d; //the length of substring
        for() { /* initialize the hash map here */ }
        while(end<s.size()){
            if(map[s[end++]]-- ?){  /* modify counter here */ }
            while(/* counter condition */){ 
                 /* update d here if finding minimum*/
                //increase begin to make it invalid/valid again
                if(map[s[begin++]]++ ?){ /*modify counter here*/ }
            }  
            /* update d here if finding maximum*/
        }
        return d;
  }

One thing needs to be mentioned is that when asked to find maximum substring, we should update maximum after the inner while loop to guarantee that the substring is valid. On the other hand, when asked to find minimum substring, we should update minimum inside the inner while loop.

The code of solving Longest Substring with At Most Two Distinct Characters is below:

int lengthOfLongestSubstringTwoDistinct(string s) {
        vector<int> map(128, 0);
        int counter=0, begin=0, end=0, d=0; 
        while(end<s.size()){
            if(map[s[end++]]++==0) counter++;
            while(counter>2) if(map[s[begin++]]--==1) counter--;
            d=max(d, end-begin);
        }
        return d;
    }

The code of solving Longest Substring Without Repeating Characters is below:

int lengthOfLongestSubstring(string s) {
        vector<int> map(128,0);
        int counter=0, begin=0, end=0, d=0; 
        while(end<s.size()){
            if(map[s[end++]]++>0) counter++; 
            while(counter>0) if(map[s[begin++]]-->1) counter--;
            d=max(d, end-begin); //while valid, update d
        }
        return d;
    }

分析:

我們可以先複製連結串列的主體部分,我們只需要使用尾插法即可。第二趟我們來複制隨機指標,因為原來連結串列中的的隨機指標指向原連結串列的節點這裡不能直接複製,因此需要將指向原連結串列節點的指標對映到新連結串列的節點,因此需要利用雜湊表建立這種對映關係。

public class Solution {
    public RandomListNode copyRandomList(RandomListNode head) {
    	RandomListNode myHead=new RandomListNode(-1),last=myHead,p=head;
    	HashMap<RandomListNode,RandomListNode> map=new HashMap<RandomListNode,RandomListNode>();
    	while(p!=null){//尾插入法
    		RandomListNode t=new RandomListNode(p.label); 
    		map.put(p, t);//建立對映關係,便於隨機指標的複製
    		last.next=t;
    		last=t;
    		p=p.next;
    	}
    	p=head;
    	while(p!=null){//複製隨機指標
    		RandomListNode t=map.get(p);
    		if(p.random==null){
    			t.random=null;
    		}else{
    			t.random=map.get(p.random);
    		}
    		p=p.next;
    	} 
		return myHead.next; 
    }
}