算法知識梳理(3) 字符串算法第二部分

1、概要

本文介紹了有關字符串的算法第二部分的Java代碼實現,全部代碼都可經過 在線編譯器 直接運行,算法目錄:java

  • 查找字符串中的最長重複子串
  • 求長度爲N的字符串的最長迴文子串
  • 將字符串中的*移到前部,而且不改變非*的順序
  • 不開闢用於交換的空間,完成字符串的逆序C++
  • 最短摘要生成
  • 最長公共子序列

2、代碼實現

2.1 查找字符串中的最長重複子串

問題描述

給定一個文本文件做爲輸入,查找其中最長的重複子字符串。例如,"Ask not what your country can do for you, but what you can do for your country"中最長的重複字符串是「can do for you」,第二長的是"your country"ios

解決思路

這裏解決問題的時候用到了 後綴數組 的思想,它指的是字符串全部右子集的集合,例如字符串abcde,它的後綴數組就爲["abcde", "bcde", "cde", "de", "e"]程序員

解法分爲三步:算法

  • 求得輸入字符串p的後綴數組,把它存放在一個List當中,這裏注意去掉空格的狀況。
  • List中的全部元素進行快速排序。快速排序的目的不在於使得整個數組有序,而在於 使得前綴差別最小的兩個字符串在數組中位於相鄰的位置,對於上面的例子,其排序結果爲:

後綴數組的快速排序結果

  • 遍歷排序後的數組,只須要對數組中的 相鄰的兩個元素 從頭開始比較,計算出這兩個字符串相同前綴的長度。遍歷以後,取得的最大值就是最長重複子串的長度,而這兩個字符串的相同前綴就是最長重複子串。

實現代碼

import java.util.ArrayList;
import java.util.List;
import java.lang.String;

class Untitled {

	static void quickSortStr(List<String> c, int start, int end){
		if(start >= end)
			return;
		int pStart = start;
		int pEnd = end;
		int pMid = start;
		String t = null;
		for (int j = pStart+1; j <= pEnd; j++) {
			if ((c.get(pStart)).compareTo(c.get(j)) > 0) {
				pMid++;
				t = c.get(pMid); 
				c.set(pMid, c.get(j)); 
				c.set(j, t);
			}
		}
		t = c.get(pStart); 
		c.set(pStart, c.get(pMid)); 
		c.set(pMid, t);
		quickSortStr(c, pStart, pMid-1);
		quickSortStr(c, pMid+1, pEnd);
	}
	
	//得到兩個字符串從第一個字符開始,相同部分的最大長度。
	static int comLen(String p1, String p2){
		int count = 0;
		int p1Index = 0;
		int p2Index = 0;
		while (p1Index < p1.length()) {
			if (p1.charAt(p1Index++) != p2.charAt(p2Index++))
				return count;
			count++;
		}
		return count;
	}

	static String longComStr(String p, int length){
		List<String> dic = new ArrayList<String>();
		int ml = 0 ;
		for (int i = 0; i < length; i++) {
			if (p.charAt(i) != ' ') {
				//構造全部的後綴數組。
				dic.add(p.substring(i, p.length()));
			}
		}
		String mp = null;
		//對後綴數組進行排序。
		quickSortStr(dic, 0, dic.size()-1);
		//打印排序後的數組用於調試。
		for (int i = 0; i < dic.size(); i++) {
			System.out.println("index=" + i + ",data=" + dic.get(i));
		}
		for (int i = 0; i < dic.size()-1; i++) {
			int tl = comLen(dic.get(i), dic.get(i+1));
			if (tl > ml) {
				ml = tl;
				mp = dic.get(i).substring(0, ml);
			}
		}
		return mp;
	} 

	public static void main(String[] args) {
		String source = "Ask not what your country can do for you, but what you can do for your country";
		System.out.println("result = " + longComStr(source, source.length()));
	}
}
複製代碼

運行結果

>> result = can do for you
複製代碼

2.2 求長度爲 N 的字符串的最長迴文子串

問題描述

長度爲N的字符串,求這個字符串裏的最長迴文子串,迴文字符串 簡單來講就是一個字符串正着讀和反着讀是同樣的。編程

解決思路

這裏用到的是Manacher算法,首先須要對原始的字符串進行預處理,即在每一個字符之間加上一個標誌位,這裏用#來表示,這會使得對於任意一個輸入,通過處理後的字符串長度爲2*len+1,也就是說 處理後的字符串始終爲奇數數組

在上面咱們已經介紹過,迴文串中最左或最右位置的字符與其對稱軸的距離稱爲 迴文半徑Manacher定義了一個數組RL[i],它表示 i個字符爲對稱軸的迴文串最右一個字符與字符i的閉區間所包含的字符個數,以google爲例,通過處理後的字符串爲#g#o#o#g#l#e,那麼RL[i]的值爲: ui

RL[i]-1的值就是原始字符串中,以位置 i爲對稱軸的最長迴文串的長度,那麼接下來的問題就變成如何計算 RL[i]數組。

首先,咱們須要兩個輔助的變量maxidRmaxid,它表示當前計算的迴文字符串中,所能觸及到的最右位置,而maxid則表示該回文串的對稱軸所在位置,而RL[maxid]爲該回文串的距離。google

假設咱們此時遍歷到了第i個字符,那麼這時候有兩種狀況:spa

(1) i < maxidR

在這種狀況下,咱們知道p[maxid+1, .., maxid+RL[maxid]-1]p[maxid-1, .., maxid-RL[maxid]+1]部分是關於p[maxid]對稱的,利用這個有效信息,能夠避免一些沒必要要的判斷。.net

如今,咱們得到i關於maxid的對稱點j,這個點位於maxid的左側,所以,咱們已經計算過以它爲中心的迴文字符串長度RL[j],對於以p[j]爲中心的迴文字符串有兩種狀況:

  • j爲中心的迴文字符的最左邊j-(RL[j]-1) 大於等於 maxidR關於maxid的對稱點maxid-(maxidR-maxid),在這種狀況下,咱們能夠推斷出以i爲對稱點的RL[i]的值最小爲RL[j]
  • 大於的狀況,能夠保證以i爲對稱點的RL[i]至少爲(maxidR-i)+1

固然這上面只是推測出的 最小狀況,以後仍然要繼續遍從來更新RL[i]的值。

(2) i >= maxidR

這時候沒有任何的已知信息,咱們只能從i的左右兩邊慢慢遍歷。

實現代碼

class Untitled {
	
	static int maxSynStr(char ip[], int len) {
		int size = 2*len + 1;
		char a[] = new char[size];
		int RL[] = new int[size];
		int i = 0;
		int n;
		while (i < len) {
			a[(i<<1)+1] = ip[i];
			a[(i<<1)+2] = '#';
			i++;
		}
		a[0] = '#';
		//最遠字符的中心對稱點。
		int maxid = 0;
		//探索到的最遠字符。
		int maxidR = 0;
		int ans = 0;
		RL[0] = 1;
		for (i = 1; i < size; i++) {
			//首先推測出i爲中心的最小回文半徑。
			int offset = 0;
			if (i < maxidR) {
				//j是關於maxid在左邊的對稱點。
				int j = maxid-(i-maxid);
				//獲取以前計算出的以j爲中心的迴文半徑。
				if (j-(RL[j]-1) >= maxid-(maxidR-maxid)) {
					offset = RL[j]-1;
				} else {
					offset = maxidR-maxid;
				} 
			}
			do {
				offset++;
			} while(i-offset >= 0 && i+offset < size && a[i+offset] == a[i-offset]);
			//最後一次是匹配失敗的,所以要減去1。
			offset--;
			//RL[i]的值包括了本身,所以要加1。
			RL[i] = offset+1;
			//更新當前最大的迴文半徑。
			if (i+offset > maxidR){
				maxidR = i+offset;
				maxid = i;
			}
			if (RL[i] > ans) {
				ans = RL[i];
			}
		}
		return ans-1;
	} 
	
	public static void main(String[] args) {
		char[] source = "google".toCharArray();
		System.out.println("result=" + maxSynStr(source, 6));
	}
}
複製代碼

運行結果:

>> result=4
複製代碼

2.3 將字符串中的 * 移到前部,而且不改變非 * 的順序

問題描述

將字符串中的*移到前部,而且不改變非*的順序,例如ab**cd**e*12,處理後爲*****abcde12

解決思路

咱們能夠將整個數組分爲兩個部分:有可能包含*字符的部分和必定不包含*字符的部分。初始時候,整個數組只有有 有可能包含*字符的部分,那麼咱們就能夠 從後往前 遍歷,每遇到一個非*的字符就把它放到 必定不包含*字符的部分,因爲須要保持非*的順序,所以須要將它插入到該部分的首部。

實現代碼

class Untitled {

	static void moveNullCharPos(char p[], int length) {
		if (length > 1) {
			char t;
			char c;
			int lastCharIndex = length;
			//必需要從後向前掃描。
			for(int j = length-1; j >=0 ;j--) {
				if ((c = p[j]) != '*') {
					lastCharIndex--;
					t = p[lastCharIndex]; p[lastCharIndex] = p[j]; p[j] = t;
				}
			}
		}
		System.out.println(p);
	}

	public static void main(String[] args) {
		char[] source = "ab**cd**e*12".toCharArray();
		moveNullCharPos(source, source.length);
	}
}
複製代碼

運行結果:

>> *****abcde12
複製代碼

2.4 不開闢用於交換的空間,完成字符串的逆序(C++)

問題描述

不開闢用於交換的空間,完成字符串的逆序。

解決思路

這裏利用的是 兩次亦或等於自己 的思想。

實現代碼

#include <iostream>
using namespace std;

void reverWithoutTemp(char *p, int length){
	int i = 0;
	int j = length-1;
	while (i < j) {
		p[i] = p[i]^p[j];  
		//其實是p[i]^p[j]^p[j],這裏的p[i]和p[j]指的是原始數組中的值。
		p[j] = p[i]^p[j];  
		//其實是(p[i]^(p[i]^p[j]^p[j]))^(p[i]^p[j]^p[j]),這裏的p[i]和p[j]指的是原始數組中的值。
		p[i] = p[i]^p[j];  
		i++;j--;
	}
	std::cout << p << std::endl;
}

int main() {
	char p[] = "1234566";
	reverWithoutTemp(p, 7);
	return 0;
}
複製代碼

運行結果:

>> 6654321
複製代碼

2.5 最短摘要生成

問題描述

給定一段描述w和一組關鍵字q,咱們從這段描述中找出包含全部關鍵字的最短字符序列,這個最短字符序列就稱爲 最短摘要

  • 最短字符序列必須包含全部的關鍵字
  • 最短字符序列中關鍵字的順序能夠是隨意的

解決思路

假設咱們的輸入序列以下所示,其中w表示非關鍵字的字符串,而q則表示關鍵字的字符串:

w0,w1,w2,w3,q0,w4,w5,q1,w6,w7,w8,q0,w9,q1
複製代碼

這裏,咱們引入額外的三個變量pStartpEndflag數組,flag數組用於統計pStartpEnd之間關鍵字的命中狀況。

這裏說明一下flag數組的做用,flag數組和關鍵字p數組的長度相同,每命中一個關鍵字,就將flag數組的對應位置+1,而flagSize只有在每次遇到一個新的關鍵字時才更新,所以它表示flag數組中 不重複的關鍵字的個數

算法的步驟以下:

  • 第一步:咱們將pEndw[0]開始移動,每發現一個命中的關鍵字,就更新flag[]數組,直到w[pStart,..,pEnd] 包含了全部的關鍵字,即w0,w1,w2,w3,q0,w4,w5,q1
  • 第二步:開始移動pStart,這時候pStart,..,pEnd之間的長度將會逐漸變短,在移動的過程當中,同時更新flag[]數組,直到pStart,...,pEnd之間 再也不包含全部的關鍵字,這時候就能夠求得 目前爲止的最短摘要長度,即q0,w4,w5,q1
  • 第三步:重複第一步的操做,移動pEnd使得pStart,...,pEnd從新 包含全部的關鍵字,再執行第二步的操做來 更新最短摘要長度,直到pEnd遍歷到w的最後一個元素。

實現代碼

class Untitled {

	static int findKey(String[] p1, String p2) {
		int len = p1.length;
		for(int i = 0; i < len; i++) {
			if(p1[i].equals(p2))
				return i;
		}
		return -1;
	}
	
	//p1爲原始數據,p2爲全部的關鍵詞。
	static int calMinAbst(String[] p1, String[] p2) {
		int p1Len = p1.length;
		int p2Len = p2.length;
		int r;
		int shortAbs = Integer.MAX_VALUE;
		int tAbs = 0;
		int pBegin = 0;
		int pEnd = 0;
		int absBegin = 0;
		int absEnd = 0;
		int flagSize = 0;
		int flag[] = new int[p2Len];
		//初始化標誌位數組。
		for (int i = 0; i < p2Len; i++) {
			flag[i] = 0;
		}
		while (pEnd < p1Len) {
			//只有先找到所有的關鍵詞才退出循環。
			while (flagSize != p2Len && pEnd < p1Len) {
				r = findKey(p2, p1[pEnd++]);
				if (r != -1) {
					if (flag[r] == 0) {
						flagSize++;
					}
					flag[r]++;
				}
			}
			while (flagSize == p2Len) {
				if ((tAbs = pEnd-pBegin) < shortAbs) {
					shortAbs = tAbs;
					absBegin = pBegin;
					absEnd = pEnd-1;
				}
				r = findKey(p2, p1[pBegin++]);
				if (r != -1) {
					flag[r]--;
					if (flag[r] == 0) {
						flagSize--;
					}
				}
			}
		}
		for (int i = absBegin; i <= absEnd; i++) {
			System.out.print(p1[i] + ",");
		}
		System.out.println("\n最短摘要長度=" + tAbs);
		return shortAbs;
	}

	public static void main(String[] args) {
		String keyword[] = {"微軟", "計算機", "亞洲"};
		String str[] = { 
			"微軟","亞洲","研究院","成立","於","1998","年",",","咱們","的","使命",
			"是","使","將來","的","計算機","可以","看","、","聽","、","學",",",
			"能","用","天然語言","與","人類","進行","交流","。","在","此","基礎","上",
			",","微軟","亞洲","研究院","還","將","促進","計算機","在","亞太","地區",
			"的","普及",",","改善","亞太","用戶","的","計算","體驗","。","」"
		};
		calMinAbst(str, keyword);
	}
}
複製代碼

運行結果

>> 微軟,亞洲,研究院,還,將,促進,計算機,
>> 最短摘要長度=7
複製代碼

2.6 最長公共子序列

問題描述

經典的LCS問題,這裏主要解釋一下最長公共子序列的含義。最長公共子串和最長公共子序列的區別:子串串的一個連續的部分子序列則是 不改變序列的順序,而從序列中去掉任意的元素 而得到的新序列。

解決思路

經典的LCS問題,原理能夠參考這篇被普遍轉載的文章 程序員編程藝術第十一章:最長公共子序列問題,這裏給出簡要介紹一下基本的思想。

LCS基於下面這個定理:

LCS 算法定理
最終目的是構建相似於下面的一個矩陣:

LCS 矩陣

  • 對於矩陣,定義c[i][j]:它表示字符串序列A的前i個字符組成的序列A和字符串序列B的前j個字符組成的序列B之間的最長公共子序列的長度,其中i<=A.len,而且j<=B.len
  • 若是A[i]=B[j],那麼AB之間的最長公共子序列的最後一項必定是這個元素,也就是c[i][j] = c[i-1][j-1]+1
  • 若是A[i]!=B[j],則c[i][j]= max(c[i-1][j], c[i][j-1])
  • 初始值爲:c[0][j]=c[i][0]=0

代碼實現

class Untitled {

	static void LCS(char a[], int aLen, char b[], int bLen){
		int c[][] = new int[bLen+1][aLen+1];
		for (int i = 1; i < bLen+1; i++) {
			for (int j = 1; j < aLen+1; j++) {
				if (a[j-1] == b[i-1]) {
					c[i][j] = c[i-1][j-1] + 1;
				} else {
					c[i][j] = (c[i-1][j]>c[i][j-1]) ? c[i-1][j]:c[i][j-1];
				}
			}
		}
		int csl = c[bLen][aLen];
		char p[] = new char[csl+1];
		int i = bLen, j = aLen;
		while (i > 0 && j > 0 && c[i][j] > 0) {
			if (c[i][j] == c[i-1][j]) {
				i--;
			} else if(c[i][j] == c[i][j-1]) {
				j--;
			} else if(c[i][j] > c[i-1][j-1]) {
				p[c[i][j]] = a[j-1];
				i--;j--;
			}
		}
		for (i = 1; i <= csl; i++) {
			System.out.print(p[i]);
		}
	} 

	public static void main(String[] args) {
		char p1[] = "aadaae".toCharArray();
		char p2[] = "adaaf".toCharArray();
		LCS(p1, p1.length, p2, p2.length);
	}
}
複製代碼

運行結果

>> adaa
複製代碼

更多文章,歡迎訪問個人 Android 知識梳理系列:

相關文章
相關標籤/搜索