模擬人腦思維,手寫智能的文本內容差異對比程序

前言

近日,筆者在工作中接到甲方提出的一項研發需求,就是用程序將修改後的文件內容和修改前的文件內容進行比對,找出其中差異的部分進行展示,以便能夠一眼看出修改人對文件做出了哪些修改。
剛接到這項需求時,感到頗有難度,但是我相信世界上沒有無算法可以解決的問題,也沒有程序實現不了的功能。經過深入思考,終於想出來實現文本內容對比的算法,並且寫成程序得以實現。現將編程思想、算法和代碼公佈,歡迎各位軟件研發人員、熱愛算法的同仁閱讀和交流。

筆者QQ:1072334275,如果任何問題,請加筆者QQ,我們一起探討算法世界的無窮奧妙。

人腦思維分析

剛開始,我對這個問題也一籌莫展,因爲一行行的去進行兩個文本對比的話,如果新文本中有增加或者刪除的行,那麼行與行的對應關係就全亂了。後來我開始想,我們人的思維在進行文本內容對比的時候是怎樣的一個過程呢?既然我們人類大腦的思維具有足夠的對比文本內容差異的智能,那麼人腦的思維過程就完全可以借鑑一下。人的思維就是最偉大的程序。
經過思考,我總結出人腦的思維在進行文本內容差異比對的時候,是以下過程:
1、首先我們的目光會將將新舊文本分別從第一行開始,逐行去看兩行的內容是否一樣。
2、當我們第一次看到兩行的內容不一樣時,我們的大腦就會想,已經開始進入內容有區別的區域了(下文稱差異區域)。
3、隨後我們會在兩個文本里面繼續往下看,直到再次看到兩個內容相同的行爲止,這時我們的大腦就會認爲內容有區別的區域已經結束。但是,我們千萬不要把問題想得這麼簡單,因爲我們的大腦做了一個我們可能不容易發現的動作,那就是試探性的將新文本或者舊文本進行整體性的上移。因爲如果在在舊文本中刪除了若干行或者在新文本中增加了若干行,那麼新舊文本之間的行與行的對應關係就會亂掉,無法通過逐行比對來找到差異區域的終點。如果是新文本中新增了若干行,則需要將新文本的所有內容整體性上移相同的行數;如果是舊文本中刪除了若干行,也需要將舊文本整體性的上移相同的行數,這樣才能對比出差異區域的終點位置。關於如何實現新舊文本的整體性上移,在下文的程序中將會詳細講解。
4、當我們的大腦分別在新舊文本里面捕捉到差異區域的開始點與結束點以後,我們的大腦的就會讓我們不要再繼續往下看了,要先分析一下差異區域裏面是被做出的怎樣的修改。
5、我們的大腦首先讀取新舊文本在差異區域各自的起點和終點,來判斷文本的變化類型。例如,如果舊文本中差異區域的起點和終點位置相同,屬於同一行,但是在新文本中差異區域的起點和終點卻隔了若干行,這時我們的思維就會直接判斷出此處的變化內容是純粹的新增了若干行。
差異區域的變化類型有5種,分別是純粹新增若干行、純粹刪除若干行、純粹修改若干行、新增並修改若干行、刪除並修改若干行。當然還有刪除並新增若干行這種情況,但是刪除之後再新增,變化的結果等同於修改,所以此種類型可以直接合併到以上5種變化類型之內。
(文本變化類型的判斷在下文會細講,因爲很重要,處理是否得當直接關係程序計算結果是否準確)
6、當我們已經知道有若干行被修改之後,我們的思維就會開始去比較那些修改前後的行的內容,觀察它們重複字符的數量,將重複字符最多的兩行認定爲修改前後的兩行。
7、當我們的大腦在已經得出第一處差異區域的變化類型和內容後,就會命令我們的眼睛從本次差異區域的終點開始(這點很重要,必須是從差異區域的終點開始),接着逐行去讀後面的文本了。在找到下一個差異區域時,重複進行上面的過程,讀取新舊文本在差異區域各自的起點和終點,判斷變化類型。然後,繼續看後面的文本。
8、直到把兩個文本內容讀完,我們的大腦就可以休息了。

以上是我們人類的大腦在進行文本內容差異對比的思維過程,讀者讀到這裏,是否已經恍然大悟了呢?接下來,請和筆者一起,依照人腦的思維過程,將大腦的活動一步一步的用程序來進行表達吧。

算法與代碼

本文的算法完全模擬人腦思維過程進行設計,因此建議讀者不要跳過“上面人腦思維分析”的內容來直接閱讀,否則會感覺晦澀難懂。

1、讀取新舊文本內容,用Map集合的鍵值對形式,將文本內容的每一行的行號和行的內容進行存儲。行號從0開始,從上到下遞增,作爲每一行的唯一標識。

//此方法用於讀取文件,並且將文件的每一行及其對應的行號存儲進Map集合進行返回
	public static Map<Integer,String> readFile(String path) {
		
		BufferedReader reader = null;
		File file = new File(path);
		if(!file.exists()) {
			System.out.println("文件不存在");
		}
		String tempStr;
        try {
        	reader = new BufferedReader(new InputStreamReader(new FileInputStream(file), "utf-8"));
        	//行號從0開始
        	int i=0;
        	Map<Integer, String> lines=new HashMap();
			while ((tempStr = reader.readLine()) != null) {
				//讀取文本時,每一行採用行號+行文本內容鍵值對的形式進行存儲,行號作爲該行的唯一標識
				lines.put(i, tempStr);
				//即將讀取下一行的時候,行號自增1
				i++;
			}
			 return lines;
		} catch (IOException e) {
			e.printStackTrace();
			return null;
		}finally {
			if(reader!=null) {
				try {
					reader.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
	}

2、有了新舊文本內容,我們將開始模擬大腦思維過程的第一步:從第一行開始,逐行比對兩個文本的行內容,直到找到差異區域的起點,就是行內容不一樣的兩行。
在程序裏面,我們可以這樣設計:
(1)通過雙重循環來遍歷存儲兩個文本行與行內容的Map集合。因爲我們要從前往後逐行對比,所以我們先把行號從大到小排序。
(2)在外層循環先遍歷舊文本的每一行,然後內層循環遍歷新文本的每一行,如果兩行的行內容相同,將存儲新文本當前行的鍵值對的value值設爲空,表示這行沒有變化後面就不用管了,後面循環時會先對新文本的行做空值判斷,若爲空值則直接跳過。以此來實現新舊文本在循環中同步換行,而不是外循環中舊文本的一行要和新文本的所有行比對。然後用break關鍵字跳出內層循環,這樣新舊文本都會直接進入下一行進行比對。
(3)如果兩行的內容不同,那麼我們就完成了大腦的第一個思維過程,找到第一個差異區域的起點。
(4)找到第一個差異區域的起點之後,我們要將新舊文本在開始進入差異區域起點的行號存儲,留待後面使用。
相關代碼如下:

//準備方法,在新舊文本尋找差異區域的起點,oldLines和newLines分別爲存儲新舊文本行內容的Map集合
	public static Map getBreakPoint(Map<Integer,String> oldLines,Map<Integer,String> newLines) {
		//定義一個集合,用於存儲差異區域的起點位置
		Map breakPoint=new HashMap();
        Object[] oldLineNums = oldLines.keySet().toArray(); 
        //將行號從小到大排序,以便能從上往下遍歷每一行
        Arrays.sort(oldLineNums); 
        //開始遍歷舊文本的每一行
        for  (Object oldLinesNum : oldLineNums) {
        	//取出舊文本中的一行以及其行號
			String lineOld=oldLines.get(oldLinesNum);
			//將行號從小到大排序,以便能從上往下遍歷每一行
			Object[] newLinesNums =  newLines.keySet().toArray(); 
		    Arrays.sort(newLinesNums); 
			//遍歷新的文本每一行
		    for  (Object newLinesNum : newLinesNums) {
		    	//取出新文本中的一行以及其行號
				String lineNew=newLines.get(newLinesNum);
				//如果新文本的行內容爲空,說明已經對比過,並且對比結果是沒有改變,因此直接跳過,
				//取出新文本的下一行來進行比對
				if(lineNew.equals("")) {
					continue;
				}else {
					//如果兩行內容不一樣,說明已經找到差異區域的起始點
					if(!lineOld.equals(lineNew)) {
						//存儲新舊文本的差異區域的起始行號,然後返回
						breakPoint.put("oldLinesBreakStart", oldLinesNum);
						breakPoint.put("newLinesBreakStart", newLinesNum);
						return breakPoint;												
					}else {
						//對於已經對比沒有改變的行,將行的內容設爲空,下次循環比對時直接跳過
						//以此來實現新舊文本的同步換行,而不是在外層的一輪循環中,內層循環要從頭開始
						newLines.put(Integer.parseInt(newLinesNum.toString()), "");
						break;
					}	
				}	
			}	
		}
        //如果對比沒有發現不相同的行,說明文本以及沒有差異,返回null值即可
        return null;	
	}

3、在上文提到,我們的大腦在捕捉到差異區域的起點之後,會繼續在兩個文本里面繼續往下看,直到再次看到兩個內容相同的行爲止,尋找差異區域的終點。
但是這個時候,差異區域的變化內容可能是新增了若干行,也可能是刪除了若干行,新舊文本之間的行對應關係可能已經錯亂。這種情況下,我們的程序採用尋找差異行起始位置使用的雙重for循環的方式,可能一次雙重循環結束都得不到我們需要的文本內容相同的行,這是行的對應關係錯誤導致。

我們要實現新舊文本的整體性上移,然後上移之後在比對文本。剛開始我想複雜了,用了遞歸結合循環來實現,其實只要用雙重循環即可。

新舊文本的整體性上移可以這樣來實現:依靠雙重for循環,因爲當內層循環結束後,外層遍歷舊文本的循環就會循環一次,也就是從下一行舊文本開始,來與新文本比對,這就實現了舊文本的上移。新文本的上移就更簡單了,內層循環本身就是逐行遍歷新文本的,所以新文本的上移在循環中自動實現。

(由於本文的算法比較複雜且抽象,可能筆者表達的不夠清晰,請讀者依據下面的程序代碼,體會這裏的計算過程。當然筆者有空時也會想辦法優化一下本文的語言描述)

程序代碼:

//準備方法,尋找差異區域的終點,也就是新舊文本重新複合的點。
	//oldLeftLines和newLeftLines分別表示存儲新舊文本從差異區域起點開始剩餘行的Map集合
	public static Map getConn(Map<Integer,String> oldLeftLines,Map<Integer,String> newLeftLines,int newLinesStart,Map reConnPoint) {
		//取出舊文本的行號集合,將行號從小到大排序,以便能從上往下遍歷每一行
		Object[] oldLinesNums = oldLeftLines.keySet().toArray(); 
        Arrays.sort(oldLinesNums);
        //取出新文本的行號集合,將行號從小到大排序,以便能從上往下遍歷每一行
        Object[] newLinesNums = newLeftLines.keySet().toArray(); 
        Arrays.sort(newLinesNums);
        int newNumMax=(int) newLinesNums[newLinesNums.length-1];
	    for (Object oldLinesNum : oldLinesNums) {
	    	String lineOld=oldLeftLines.get(oldLinesNum);
			int oldNum=Integer.valueOf(oldLinesNum.toString());
			for(Object newLinesNum : newLinesNums) {
				int newNum=Integer.valueOf(newLinesNum.toString());
				//找到內容相同的行
				if(newLeftLines.get(newNum).equals(oldLeftLines.get(oldNum))) {
					//已經找到內容相同的行,可以認爲差異區域的結束,記錄下差異區域終點位置然後返回結果
					reConnPoint.put("oldLinesConnPoint", oldNum);
					reConnPoint.put("newLinesConnPoint", newNum);
					return reConnPoint;
				}
			}		
		}
	    return reConnPoint;
	}

4、現在我們已經得到差異區域的起點和終點了,不知道讀者還是否記得上文所說的,這時我們的大腦不會再讓我們往下閱讀,而是轉而判斷差異區域的變更類型。
關於如何判斷差異區域的變化類型,需要使用新舊文本的差異區域的起點行號與終點行號來進行分析,這裏我們不妨先定義一下,以方便書寫:

舊文本差異區域的起點行號爲oldStart,舊文本差異區域的終點行號爲oldEnd;
新文本差異區域的起點行號爲newStart,新文本差異區域的終點行號爲newEnd;

然後,我來爲你演示每種變化類型:
(1)、如果oldStart=oldEnd&&newStart<newEnd,說明變化類型是純粹的新增了若干行:
如下面所示,我們在新文本中新增了一行,內容爲“ddd”。那麼舊文本中差異區域的起點和終點均爲第2行,即內容爲“bbb”行,新文本中差異區域的起點和終點則分別爲第2行和第3行。
![簡單的新文本中新增一行的表示](https://img-blog.csdnimg.cn/20191110203619280.png
(2)、如果oldEnd<oldStart&&newEnd<newStart&&(oldEnd-oldStart)==(newEnd-newStart),則說明變化類型是純粹的修改了若干行:
如下圖所示:我們將新文本中第二行的內容由“bbb”修改爲“ddd”,則舊文本中差異區域的起點和終點分別爲2和3,新文本中差異區域的起點和終點則同樣分別爲第2行和第3行。
簡單的新文本中修改某行的表示
後面的變化類型筆者不再作圖演示,請讀者按照相同的方法自行推導,這裏直接給出結論。

(3)如果(oldEnd-oldStart)>(newEnd-newStart)&&newEnd==newStart,說明文本的變化類型是純粹的被刪除了若干行。
(4)如果oldEnd!=oldStart&&newEnd!=newStart&&(oldEnd-oldStart)<(newEnd-newStart),說明變化類型是既有新增也有修改了若干行。
(5)如果oldEnd!=oldStart&&newEnd!=newStart&&(oldEnd-oldStart)>(newEnd-newStart),說明變化類型是既有刪除也有修改了若干行。

等確定變化類型之後,如果有被修改的行,我們需要利用第5步的方法找出修改前後兩行的對應關係。然後,就將本輪對比的結果儲存起來吧。

//準備方法,分析文本的變化類型,存入結果集合中
	public static void analType(int newStart,int newEnd,int oldStart,int oldEnd,Map<Integer,String> newLines,Map<Integer,String> oldLines,int total) {
		
		//下面開始分析差異區域的變化類型,然後按照類型進行處理
		if((oldEnd-oldStart)>(newEnd-newStart)&&newEnd==newStart) {
			Map oldline=new HashMap();
			for(int i=oldStart;i<oldEnd;i++) {
				//取出被刪除的行,存入集合
				oldline.put(i, oldLines.get(i));
			}
			//resultMap靜態變量表示最後總的結果,由於本方法會遞歸執行,total值會隨着本方法的遞歸自增
			//以此來讓下次遞歸時存入的計算結果與本輪的計算結果key值不同,避免前面遞歸的計算結果被覆蓋
			resultMap.put("delete"+total,oldline );
		}
		
		if((oldEnd-oldStart)==(newEnd-newStart)) {
			Map oldline=new HashMap();
			Map newline=new HashMap();
			for(int i1=oldStart;i1<oldEnd;i1++) {
				oldline.put(i1, oldLines.get(i1));
			}
			for(int i2=newStart;i2<newEnd;i2++) {
				newline.put(i2, newLines.get(i2));
			}
			
			//收集修改的行,調用下面的方法,進行新舊行匹配
			int number=oldEnd-oldStart;
			Map<Integer, Integer> change=getUpdateLines(oldline,newline,number);
			resultMap.put("update"+total,change );
		}
		
		if(oldEnd==oldStart&&(oldEnd-oldStart)<(newEnd-newStart)) {
			Map newline=new HashMap();
			for(int i=newStart;i<newEnd;i++) {
				newline.put(i, newLines.get(i));		
			}
			resultMap.put("add"+total,newline );
		}
		
		//說明有新增也有修改
		if(oldEnd!=oldStart&&newEnd!=newStart&&(oldEnd-oldStart)<(newEnd-newStart)) {
			//此時修改的行數是:
			int number=oldEnd-oldStart;
			Map<Integer,String> oldline=new HashMap();
			Map<Integer,String> newline=new HashMap();
			Map<Integer,String> addline=new HashMap();
			
			for(int i1=oldStart;i1<oldEnd;i1++) {
				oldline.put(i1, oldLines.get(i1));
			}
			for(int i2=newStart;i2<newEnd;i2++) {
				newline.put(i2, newLines.get(i2));
			}
			//獲取修改的舊文本行號與新文本行號組成鍵值對的集合
			Map<Integer, Integer> change=getUpdateLines(oldline,newline,number);
			resultMap.put("update"+total,change );
			//獲取新增的行
			for(Integer lineNum1:newline.keySet()) {
				//m是用來檢測是否屬於修改的行的一個標誌,初始值設爲0
				int m=0;
				for(Integer lineNum2:change.keySet()) {
					//說明這是修改的行
					if(lineNum1==change.get(lineNum2)) {
						m++;
					}
				}
				//當內部循環結束,如果m沒有自增,說明這不是修好的行,而是增加的行
				if(m==0) {
					addline.put(lineNum1, newline.get(lineNum1));
				}
			}
			resultMap.put("add"+total,addline);	
		}
		
		
		//說明有刪除也有修改
		if(oldEnd!=oldStart&&newEnd!=newStart&&(oldEnd-oldStart)>(newEnd-newStart)) {
			int number=newEnd-newStart;
			Map<Integer,String> oldline=new HashMap();
			Map<Integer,String> newline=new HashMap();
			
			Map<Integer,String> addline=new HashMap();
			
			for(int i1=oldStart;i1<oldEnd;i1++) {
				oldline.put(i1, oldLines.get(i1));
			}
			for(int i2=newStart;i2<newEnd;i2++) {
				newline.put(i2, newLines.get(i2));
			}
			
			//獲取修改的行
			Map<Integer, Integer> change=getUpdateLines(oldline,newline,number);
			resultMap.put("update"+total,change );
			for(Integer lineNum1:oldline.keySet()) {
				//m用來標誌是否屬於修改的行
				int m=0;
				for(Integer lineNum2:change.keySet()) {
					//說明這是修改的行
					if(lineNum1==lineNum2) {
						m++;
					}
				}
				//當內部循環結束,如果m沒有自增,說明這不是修好的行,而是增加的行
				if(m==0) {
					addline.put(lineNum1, oldline.get(lineNum1));
				}
			}
			resultMap.put("delete"+total,addline);
		}
	}

5、在確定了差異區域的變化類型後,我們要使得程序具有高度智能的特性,就需要再進行一步計算。即如果變化類型具有修改了若干行,我們要使用程序分析出修改前的行與修改後的行之間的關聯關係,即在頁面上不僅展示舊文件裏哪一行被修改了,還要展示修改後的行是新文件的哪一行。
如果尋找修改前後的兩行關聯,筆者採用的方法是將新舊文本差異區域內的行均取出,兩兩組合,計算每對組合中,兩行的重複字符數量。例如如果有3行被修改了,我們取重複字符個數最多的3對組合作爲修改前後行的關聯關係。
說到這裏,你可能不太明白,我舉個例子吧。
現在有a,b,c三個舊文本的行,和d,e兩個新文本的行組成的差異區域,字母均表示行內容。我們利用上面的分析方法很輕易的可以知道,有一行被刪除了,有兩行被修改了。於是我們將新舊文本的行進行兩兩組隊,例如a與d組隊,然後計算a和d的重複字符數量。新舊文本的行必須全部組隊,採用雙重for循環來完成。如果a與d、b與e這兩對的重複字符數量最多,則我們可以任務a被修改爲d,b被修改爲e,c行被刪除了。

以下爲程序代碼

//準備方法,計算兩個字符串相同字符的數量
	public static int numJewelsInStones(String J, String S) {
		J=J.trim();
		S=S.trim();
        char[] Ja = J.toCharArray();
        char[] Sa = S.toCharArray();
        int r = 0;
        for (int i = 0;i < Ja.length ; i ++){
            for(int j = 0; j < Sa.length; j++){
                if(Ja[i] == Sa[j])
                    r ++;
            }
        }
        return r;
    }
	
	//準備方法,將Map集合按照Value值進行排序
    public static List<String> sortMapByValue(Map<String, Integer> map) {
        int size = map.size();
        //通過map.entrySet()將map轉換爲"1.B.1.e=78"形式的list集合
        List<Map.Entry<String, Integer>> list = new ArrayList<Map.Entry<String, Integer>>(size);
        list.addAll(map.entrySet());
        //通過Collections.sort()排序
        Collections.sort(list, new ValueComparator());
        List<String> keys = new ArrayList<String>(size);
        for (Entry<String, Integer> entry : list){
            // 得到排序後的鍵值
            keys.add(entry.getKey());
        }
        return keys;
    }

    private static class ValueComparator implements Comparator<Map.Entry<String, Integer>> {
        public int compare(Map.Entry<String, Integer> o1, Map.Entry<String, Integer> o2) {
            // compareTo方法 (x < y) ? -1 : ((x == y) ? 0 : 1)
            // 倒序:o2.getValue().compareTo(o1.getValue()),順序:o1.getValue().compareTo(o2.getValue())
            return o2.getValue().compareTo(o1.getValue());
        }
    }
	
	//準備方法,找出修改的哪些行。contentOld和contentNew分別表示新舊文本里面在差異區域內的行的集合
	//參數n表示我們需要找的修改前後的行有幾對
	public static Map getUpdateLines(Map<Integer,String> contentOld,Map<Integer,String> contentNew,int n) {
		
		Map<Integer, Integer> resultMap=new HashMap();
		//準備集合,用來儲存組隊兩行的重複字符個數與各自的行號
		Map<String,Integer>samChar=new HashMap();
		for(Integer oldNum:contentOld.keySet()) {
			for(Integer newNum:contentNew.keySet()) {
				//比較兩行之間相同字符的數量
				int count=numJewelsInStones(contentOld.get(oldNum),contentNew.get(newNum));
				//將每兩行之間的相同字符數量和行號存入集合
				samChar.put(oldNum.toString()+":"+newNum.toString(),count);
			}
		}	
		//獲取按照value值(也就是重複字符個數)從大到小排序的key值集合,以便取出重複字符最對的組隊。
		List<String> keys=sortMapByValue(samChar);

        //取出相同字符數量最多的新舊行對
        for(int i=0;i<n;i++) {
        	String lineNumArr=keys.get(i);
        	String[] lineNumA=lineNumArr.split(":");
            //重複字符最多的行對視爲修改前後的兩行
        	resultMap.put(Integer.valueOf(lineNumA[0]),Integer.valueOf(lineNumA[1]));
        	
        }
        return resultMap;
	}

6、此時一次差異區域的分析已經完成,這個時候,我們的大腦思維就開始要求我們的目光從本次差異區域的終點開始,繼續往下逐行比對文本內容了。因此,我們首先要將新舊文本從各自差異區域的終點開始,到集合結尾之間剩餘的行收集起來,組成新的行號與行內容鍵值對的集合,然後採用遞歸的方法讓程序整體重新執行一次,即開始新一輪的尋找差異區域。

//準備方法,循環比對文本
	public static void compare(Map<Integer,String> oldLines,Map<Integer,String> newLines,int total) {

		Map breakPoint=getBreakPoint(oldLines,newLines);
		if(breakPoint!=null) {
			int oldStart=(int) breakPoint.get("oldLinesBreakStart");
			int newStart=(int) breakPoint.get("newLinesBreakStart");

			//將從差異區域起始點點之後全部的行存入新的集合,以便後面再尋找差異區域的結束位置
			Map<Integer, String> oldLeftLines=new HashMap();
			Map<Integer, String> newLeftLines=new HashMap();
			
			for(Integer oldLinesNum:oldLines.keySet()) {
				if(oldLinesNum>=oldStart) {
					
					oldLeftLines.put(oldLinesNum, oldLines.get(oldLinesNum));
				}
			}
			for(Integer newLinesNum:newLines.keySet()) {
				if(newLinesNum>=newStart) {
					newLeftLines.put(newLinesNum, newLines.get(newLinesNum));
				}
			}
			
		
			int newLinesStart=0;
			
			Map reConnPoint=new HashMap();
			//調用方法,尋找差異區域的終點位置
			reConnPoint=getConn(oldLeftLines,newLeftLines,newLinesStart,reConnPoint);
            //如果找到了終點位置
			if(reConnPoint.get("oldLinesConnPoint")!=null) {
				int oldEnd=(int) reConnPoint.get("oldLinesConnPoint");
				int newEnd=(int) reConnPoint.get("newLinesConnPoint");
				//調用方法,分析差異區域的變動類型
				analType(newStart,newEnd,oldStart,oldEnd,newLines,oldLines,total);
				
				//取出新舊文本中剩餘行的集合,準備使用遞歸進行新一輪的尋找差異區域……
				Map<Integer,String> nextOldLines=new HashMap();
				Map<Integer,String> nextNewLines=new HashMap();
				for(int oldLinseNum:oldLines.keySet()) {
					if(oldLinseNum>=oldEnd) {
						nextOldLines.put(oldLinseNum, oldLines.get(oldLinseNum));
					}
				}
				
				for(int newLinesNum:newLines.keySet()) {
					if(newLinesNum>=newEnd) {
						nextNewLines.put(newLinesNum, newLines.get(newLinesNum));
					}
				}
				total++;
				//遞歸執行本方法,相當於我們的大腦繼續向下讀文本內容,尋找新的差異區域
				compare(nextOldLines,nextNewLines,total);
			//如果差異區域沒有終點,說明用戶是在文件的最後修改的,爲了程序計算,新增一個虛擬的行表示終點
			}else {
				
				Object[] oldLineNums = oldLines.keySet().toArray();
				Arrays.sort(oldLineNums);	 
				int oldEnd=Integer.valueOf(oldLineNums[oldLineNums.length-1].toString())+1;
				
				Object[] newLineNums = newLines.keySet().toArray();
				Arrays.sort(newLineNums);	 
				int newEnd=Integer.valueOf(newLineNums[newLineNums.length-1].toString())+1;
				analType(newStart,newEnd,oldStart,oldEnd,newLines,oldLines,total);
				
			}
		}
	}

完整代碼

下面獻上程序的完整代碼

package com.haibo.anal;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

public class CompareFile {
	static Map<String,Map> resultMap=new HashMap();	public static void main(String[] args) {
		resultMap.clear();
		String path1="E://comparetest/1.txt";
		String path2="E://comparetest/2.txt";
		Map<Integer,String> oldLines=readFile(path1);
		Map<Integer,String> newLines=readFile(path2);
		
		int total=0;
		compare(oldLines,newLines, total);
		
		//展示結果
		System.out.println();
		System.out.println("對比結果展示:");
		System.out.println();
		for(String changeType:resultMap.keySet()){
			if(changeType.contains("add")) {
				Map<Integer,String> addLines=resultMap.get(changeType);
				for(Integer linesNum:addLines.keySet()) {
					System.out.println("新文本中新增了第"+linesNum+"行,內容爲:“"+addLines.get(linesNum)+"”");
				}
			}
			
			
			
			if(changeType.contains("delete")) {
				Map<Integer,String> delLines=resultMap.get(changeType);
				for(Integer linesNum:delLines.keySet()) {
					System.out.println("舊文本中刪除了第"+linesNum+"行,內容爲:“"+delLines.get(linesNum)+"”");
				}
			}
			
			if(changeType.contains("update")) {
				Map<Integer,Integer> updateLines=resultMap.get(changeType);
				for(Integer linesNum:updateLines.keySet()) {
					System.out.println("舊文本中的第"+linesNum+"行,內容爲:“"+oldLines.get(linesNum)
					+"”,被修改爲新文本中的第"+updateLines.get(linesNum)+"行,內容爲“"+newLines.get(updateLines.get(linesNum))+"”");
				}
			}

		}
		

	}
	
	
	//準備方法,循環比對文本
	public static void compare(Map<Integer,String> oldLines,Map<Integer,String> newLines,int total) {

		Map breakPoint=getBreakPoint(oldLines,newLines);
		if(breakPoint!=null) {
			int oldStart=(int) breakPoint.get("oldLinesBreakStart");
			int newStart=(int) breakPoint.get("newLinesBreakStart");

			//將從差異區域起始點點之後全部的行存入新的集合,以便後面再尋找差異區域的結束位置
			Map<Integer, String> oldLeftLines=new HashMap();
			Map<Integer, String> newLeftLines=new HashMap();
			
			for(Integer oldLinesNum:oldLines.keySet()) {
				if(oldLinesNum>=oldStart) {
					
					oldLeftLines.put(oldLinesNum, oldLines.get(oldLinesNum));
				}
			}
			for(Integer newLinesNum:newLines.keySet()) {
				if(newLinesNum>=newStart) {
					newLeftLines.put(newLinesNum, newLines.get(newLinesNum));
				}
			}
			
		
			int newLinesStart=0;
			
			Map reConnPoint=new HashMap();
			//調用方法,尋找差異區域的終點位置
			reConnPoint=getConn(oldLeftLines,newLeftLines,newLinesStart,reConnPoint);
            //如果找到了終點位置
			if(reConnPoint.get("oldLinesConnPoint")!=null) {
				int oldEnd=(int) reConnPoint.get("oldLinesConnPoint");
				int newEnd=(int) reConnPoint.get("newLinesConnPoint");
				//調用方法,分析差異區域的變動類型
				analType(newStart,newEnd,oldStart,oldEnd,newLines,oldLines,total);
				
				//取出新舊文本中剩餘行的集合,準備使用遞歸進行新一輪的尋找差異區域……
				Map<Integer,String> nextOldLines=new HashMap();
				Map<Integer,String> nextNewLines=new HashMap();
				for(int oldLinseNum:oldLines.keySet()) {
					if(oldLinseNum>=oldEnd) {
						nextOldLines.put(oldLinseNum, oldLines.get(oldLinseNum));
					}
				}
				
				for(int newLinesNum:newLines.keySet()) {
					if(newLinesNum>=newEnd) {
						nextNewLines.put(newLinesNum, newLines.get(newLinesNum));
					}
				}
				total++;
				//遞歸執行本方法,相當於我們的大腦繼續向下讀文本內容,尋找新的差異區域
				compare(nextOldLines,nextNewLines,total);
			//如果差異區域沒有終點,說明用戶是在文件的最後修改的,爲了程序計算,新增一個虛擬的行表示終點
			}else {
				
				Object[] oldLineNums = oldLines.keySet().toArray();
				Arrays.sort(oldLineNums);	 
				int oldEnd=Integer.valueOf(oldLineNums[oldLineNums.length-1].toString())+1;
				
				Object[] newLineNums = newLines.keySet().toArray();
				Arrays.sort(newLineNums);	 
				int newEnd=Integer.valueOf(newLineNums[newLineNums.length-1].toString())+1;
				analType(newStart,newEnd,oldStart,oldEnd,newLines,oldLines,total);
				
			}
		}
	}
	
	//準備方法,分析文本的變化類型,存入結果集合中
	public static void analType(int newStart,int newEnd,int oldStart,int oldEnd,Map<Integer,String> newLines,Map<Integer,String> oldLines,int total) {
		
		//下面開始分析差異區域的變化類型,然後按照類型進行處理
		if((oldEnd-oldStart)>(newEnd-newStart)&&newEnd==newStart) {
			Map oldline=new HashMap();
			for(int i=oldStart;i<oldEnd;i++) {
				//取出被刪除的行,存入集合
				oldline.put(i, oldLines.get(i));
			}
			//resultMap靜態變量表示最後總的結果,由於本方法會遞歸執行,total值會隨着本方法的遞歸自增
			//以此來讓下次遞歸時存入的計算結果與本輪的計算結果key值不同,避免前面遞歸的計算結果被覆蓋
			resultMap.put("delete"+total,oldline );
		}
		
		if((oldEnd-oldStart)==(newEnd-newStart)) {
			Map oldline=new HashMap();
			Map newline=new HashMap();
			for(int i1=oldStart;i1<oldEnd;i1++) {
				oldline.put(i1, oldLines.get(i1));
			}
			for(int i2=newStart;i2<newEnd;i2++) {
				newline.put(i2, newLines.get(i2));
			}
			
			//收集修改的行,調用下面的方法,進行新舊行匹配
			int number=oldEnd-oldStart;
			Map<Integer, Integer> change=getUpdateLines(oldline,newline,number);
			resultMap.put("update"+total,change );
		}
		
		if(oldEnd==oldStart&&(oldEnd-oldStart)<(newEnd-newStart)) {
			Map newline=new HashMap();
			for(int i=newStart;i<newEnd;i++) {
				newline.put(i, newLines.get(i));		
			}
			resultMap.put("add"+total,newline );
		}
		
		//說明有新增也有修改
		if(oldEnd!=oldStart&&newEnd!=newStart&&(oldEnd-oldStart)<(newEnd-newStart)) {
			//此時修改的行數是:
			int number=oldEnd-oldStart;
			Map<Integer,String> oldline=new HashMap();
			Map<Integer,String> newline=new HashMap();
			Map<Integer,String> addline=new HashMap();
			
			for(int i1=oldStart;i1<oldEnd;i1++) {
				oldline.put(i1, oldLines.get(i1));
			}
			for(int i2=newStart;i2<newEnd;i2++) {
				newline.put(i2, newLines.get(i2));
			}
			//獲取修改的舊文本行號與新文本行號組成鍵值對的集合
			Map<Integer, Integer> change=getUpdateLines(oldline,newline,number);
			resultMap.put("update"+total,change );
			//獲取新增的行
			for(Integer lineNum1:newline.keySet()) {
				//m是用來檢測是否屬於修改的行的一個標誌,初始值設爲0
				int m=0;
				for(Integer lineNum2:change.keySet()) {
					//說明這是修改的行
					if(lineNum1==change.get(lineNum2)) {
						m++;
					}
				}
				//當內部循環結束,如果m沒有自增,說明這不是修好的行,而是增加的行
				if(m==0) {
					addline.put(lineNum1, newline.get(lineNum1));
				}
			}
			resultMap.put("add"+total,addline);	
		}
		
		
		//說明有刪除也有修改
		if(oldEnd!=oldStart&&newEnd!=newStart&&(oldEnd-oldStart)>(newEnd-newStart)) {
			int number=newEnd-newStart;
			Map<Integer,String> oldline=new HashMap();
			Map<Integer,String> newline=new HashMap();
			
			Map<Integer,String> addline=new HashMap();
			
			for(int i1=oldStart;i1<oldEnd;i1++) {
				oldline.put(i1, oldLines.get(i1));
			}
			for(int i2=newStart;i2<newEnd;i2++) {
				newline.put(i2, newLines.get(i2));
			}
			
			//獲取修改的行
			Map<Integer, Integer> change=getUpdateLines(oldline,newline,number);
			resultMap.put("update"+total,change );
			for(Integer lineNum1:oldline.keySet()) {
				//m用來標誌是否屬於修改的行
				int m=0;
				for(Integer lineNum2:change.keySet()) {
					//說明這是修改的行
					if(lineNum1==lineNum2) {
						m++;
					}
				}
				//當內部循環結束,如果m沒有自增,說明這不是修好的行,而是增加的行
				if(m==0) {
					addline.put(lineNum1, oldline.get(lineNum1));
				}
			}
			resultMap.put("delete"+total,addline);
		}
	}
	
	
	//準備方法,在新舊文本尋找差異區域的起點,oldLines和newLines分別爲存儲新舊文本行內容的Map集合
	public static Map getBreakPoint(Map<Integer,String> oldLines,Map<Integer,String> newLines) {
		//定義一個集合,用於存儲差異區域的起點位置
		Map breakPoint=new HashMap();
        Object[] oldLineNums = oldLines.keySet().toArray(); 
        //將行號從小到大排序,以便能從上往下遍歷每一行
        Arrays.sort(oldLineNums); 
        //開始遍歷舊文本的每一行
        for  (Object oldLinesNum : oldLineNums) {
        	//取出舊文本中的一行以及其行號
			String lineOld=oldLines.get(oldLinesNum);
			//將行號從小到大排序,以便能從上往下遍歷每一行
			Object[] newLinesNums =  newLines.keySet().toArray(); 
		    Arrays.sort(newLinesNums); 
			//遍歷新的文本每一行
		    for  (Object newLinesNum : newLinesNums) {
		    	//取出新文本中的一行以及其行號
				String lineNew=newLines.get(newLinesNum);
				//如果新文本的行內容爲空,說明已經對比過,並且對比結果是沒有改變,因此直接跳過,
				//取出新文本的下一行來進行比對
				if(lineNew.equals("")) {
					continue;
				}else {
					//如果兩行內容不一樣,說明已經找到差異區域的起始點
					if(!lineOld.equals(lineNew)) {
						//存儲新舊文本的差異區域的起始行號,然後返回
						breakPoint.put("oldLinesBreakStart", oldLinesNum);
						breakPoint.put("newLinesBreakStart", newLinesNum);
						return breakPoint;												
					}else {
						//對於已經對比沒有改變的行,將行的內容設爲空,下次循環比對時直接跳過
						//以此來實現新舊文本的同步換行,而不是在外層的一輪循環中,內層循環要從頭開始
						newLines.put(Integer.parseInt(newLinesNum.toString()), "");
						break;
					}	
				}	
			}	
		}
        //如果對比沒有發現不相同的行,說明文本以及沒有差異,返回null值即可
        return null;	
	}
	
	
	//準備方法,尋找差異區域的終點,也就是新舊文本重新複合的點。
	//oldLeftLines和newLeftLines分別表示存儲新舊文本從差異區域起點開始剩餘行的Map集合
	public static Map getConn(Map<Integer,String> oldLeftLines,Map<Integer,String> newLeftLines,int newLinesStart,Map reConnPoint) {
		//取出舊文本的行號集合,將行號從小到大排序,以便能從上往下遍歷每一行
		Object[] oldLinesNums = oldLeftLines.keySet().toArray(); 
        Arrays.sort(oldLinesNums);
        //取出新文本的行號集合,將行號從小到大排序,以便能從上往下遍歷每一行
        Object[] newLinesNums = newLeftLines.keySet().toArray(); 
        Arrays.sort(newLinesNums);
        int newNumMax=(int) newLinesNums[newLinesNums.length-1];
	    for (Object oldLinesNum : oldLinesNums) {
	    	String lineOld=oldLeftLines.get(oldLinesNum);
			int oldNum=Integer.valueOf(oldLinesNum.toString());
			for(Object newLinesNum : newLinesNums) {
				int newNum=Integer.valueOf(newLinesNum.toString());
				//找到內容相同的行
				if(newLeftLines.get(newNum).equals(oldLeftLines.get(oldNum))) {
					//已經找到內容相同的行,可以認爲差異區域的結束,記錄下差異區域終點位置然後返回結果
					reConnPoint.put("oldLinesConnPoint", oldNum);
					reConnPoint.put("newLinesConnPoint", newNum);
					return reConnPoint;
				}
			}		
		}
	    return reConnPoint;
	}
	
	
	
	
	//此方法用於讀取文件,並且將文件的每一行及其對應的行號存儲進Map集合進行返回
	public static Map<Integer,String> readFile(String path) {
		
		BufferedReader reader = null;
		File file = new File(path);
		if(!file.exists()) {
			System.out.println("文件不存在");
		}
		String tempStr;
        try {
        	reader = new BufferedReader(new InputStreamReader(new FileInputStream(file), "utf-8"));
        	//行號從0開始
        	int i=0;
        	Map<Integer, String> lines=new HashMap();
			while ((tempStr = reader.readLine()) != null) {
				//讀取文本時,每一行採用行號+行文本內容鍵值對的形式進行存儲,行號作爲該行的唯一標識
				lines.put(i, tempStr);
				//即將讀取下一行的時候,行號自增1
				i++;
			}
			 return lines;
		} catch (IOException e) {
			e.printStackTrace();
			return null;
		}finally {
			if(reader!=null) {
				try {
					reader.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
	}
	
	//準備方法,計算兩個字符串相同字符的數量
	public static int numJewelsInStones(String J, String S) {
		J=J.trim();
		S=S.trim();
        char[] Ja = J.toCharArray();
        char[] Sa = S.toCharArray();
        int r = 0;
        for (int i = 0;i < Ja.length ; i ++){
            for(int j = 0; j < Sa.length; j++){
                if(Ja[i] == Sa[j])
                    r ++;
            }
        }
        return r;
    }
	
	//準備方法,將Map集合按照Value值進行排序
    public static List<String> sortMapByValue(Map<String, Integer> map) {
        int size = map.size();
        //通過map.entrySet()將map轉換爲"1.B.1.e=78"形式的list集合
        List<Map.Entry<String, Integer>> list = new ArrayList<Map.Entry<String, Integer>>(size);
        list.addAll(map.entrySet());
        //通過Collections.sort()排序
        Collections.sort(list, new ValueComparator());
        List<String> keys = new ArrayList<String>(size);
        for (Entry<String, Integer> entry : list){
            // 得到排序後的鍵值
            keys.add(entry.getKey());
        }
        return keys;
    }

    private static class ValueComparator implements Comparator<Map.Entry<String, Integer>> {
        public int compare(Map.Entry<String, Integer> o1, Map.Entry<String, Integer> o2) {
            // compareTo方法 (x < y) ? -1 : ((x == y) ? 0 : 1)
            // 倒序:o2.getValue().compareTo(o1.getValue()),順序:o1.getValue().compareTo(o2.getValue())
            return o2.getValue().compareTo(o1.getValue());
        }
    }
	
	//準備方法,找出修改的哪些行。contentOld和contentNew分別表示新舊文本里面在差異區域內的行的集合
	//參數n表示我們需要找的修改前後的行有幾對
	public static Map getUpdateLines(Map<Integer,String> contentOld,Map<Integer,String> contentNew,int n) {
		
		Map<Integer, Integer> resultMap=new HashMap();
		//準備集合,用來儲存組隊兩行的重複字符個數與各自的行號
		Map<String,Integer>samChar=new HashMap();
		for(Integer oldNum:contentOld.keySet()) {
			for(Integer newNum:contentNew.keySet()) {
				//比較兩行之間相同字符的數量
				int count=numJewelsInStones(contentOld.get(oldNum),contentNew.get(newNum));
				//將每兩行之間的相同字符數量和行號存入集合
				samChar.put(oldNum.toString()+":"+newNum.toString(),count);
			}
		}	
		//獲取按照value值(也就是重複字符個數)從大到小排序的key值集合,以便取出重複字符最對的組隊。
		List<String> keys=sortMapByValue(samChar);

        //取出相同字符數量最多的新舊行對
        for(int i=0;i<n;i++) {
        	String lineNumArr=keys.get(i);
        	String[] lineNumA=lineNumArr.split(":");
            //重複字符最多的行對視爲修改前後的兩行
        	resultMap.put(Integer.valueOf(lineNumA[0]),Integer.valueOf(lineNumA[1]));
        	
        }
        return resultMap;
	}
}

程序計算結果測試

這是用於對比的初始文本(即完整代碼中的“E://comparetest/1.txt”):

漢皇重色思傾國,
御宇多年求不得。
楊家有女初長成,
養在深閨人未識。
天生麗質難自棄,
一朝選在君王側。
回眸一笑百媚生,
六宮粉黛無顏色。
春寒賜浴華清池,
溫泉水滑洗凝脂。
侍兒扶起嬌無力,
始是新承恩澤時。
雲鬢花顏金步搖,
芙蓉帳暖度春宵。
春宵苦短日高起,
從此君王不早朝。

我們將其修改爲(即完整代碼中的“E://comparetest/2.txt”):

漢皇重色思傾國,
美人多年求不得。
楊家有女初成年,
養在深閨人未識。
天生麗質難自棄,
回頭一笑百媚生,
六宮粉黛無顏色。
春寒賜浴華清池,
溫泉水滑洗凝脂。
侍兒扶起嬌無力,
始是新承恩澤時。
後宮佳麗三千人,
三千寵愛在一身。
雲鬢花顏搖啊搖,
芙蓉帳暖度春宵。
春宵苦短日高起,
從此君王不上朝。

控制檯打印的對比結果是(結果準確無誤):
文本對比結果

題外話

這個程序是模仿人腦的思維過程進行計算的,有時候我在想,智慧生物的思維是不是宇宙中的一種超自然力(例如宗教信徒信仰的上帝)所編寫的一個程序呢?人的大腦相當於CPU,由於CPU的性能不同,導致人與人之間的智商有差異。

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